Skip to main content

Authentication

Every request authenticates itself: you build a canonical string from that request's own fields, sign it with your RSA private key, and send the signature as a signature field in the JSON body.

AliX signs its replies back to you the same way, with a different key, so you can prove a response really came from us.

What AliX issues you

At onboarding you receive four things.

partnerCodeIdentifies you. It goes in the body of nearly every request.
secretKeyA shared secret. It is never transmitted; it is the tail of every signed string.
Your public keyYou generate an RSA-2048 key pair and give AliX the public half. It verifies your requests.
AliX's public keyIssued to you. You verify AliX's response and webhook signatures with it.

Your private key never leaves your server. AliX's key is issued per partner, so verify with the one issued to you.

AliX also allowlists the IPs you call from. A request from any other address fails with PARTNER_IP_NOT_ALLOWED (errorCode 19), however good the signature is. The allowlist is checked on every request.

Generating your key pair

The pair is yours to generate; AliX never creates it for you and never sees the private half. Two OpenSSL commands produce it:

Bash
openssl genrsa -out partner-private.pem 2048
openssl rsa -in partner-private.pem -pubout -out partner-public.pem

partner-private.pem is the key your signing code reads — the samples on this page load exactly this file. partner-public.pem is the half you give AliX at onboarding, and it is plain PEM text:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA48/RNJOibAJlduffMtqy

uwIDAQAB
-----END PUBLIC KEY-----

Send it exactly as the file holds it, BEGIN line to END line. A PKCS#1 export (-----BEGIN RSA PUBLIC KEY-----) is accepted too. The private key's header varies with the OpenSSL release — PRIVATE KEY on current ones, RSA PRIVATE KEY on older — and either works with the signing code below.

Generating the pair in a browser

8gwifi.org's RSA tool does the same job without installing anything: it opens with a fresh 2048-bit pair in the Public Key and Private Key boxes, in the same PEM forms as above. Save each box into its own file.

The same page signs and verifies test strings. If you use it for that, select SHA256withRSA under RSA Signature Algorithms — the default, RSASSA-PSS, is a different scheme, and AliX rejects its signatures with INVALID_SIGNATURE (errorCode 2).

Keys generated in a browser are best treated as sandbox material; generate the production pair with OpenSSL, on the machine that keeps it.

Signing a request

The signed string is a fixed, ordered, pipe-joined subset of the request's fields — not the JSON body — with your secretKey on the end:

<field 1>|<field 2>|…|<field n>|<secretKey>

Which fields go into it, and in what order, is specific to each endpoint, so every endpoint page prints its exact string — build from the printed string rather than from the body. A field out of place returns INVALID_SIGNATURE (errorCode 2); when it does, recheck the field order against the endpoint page.

Three things about the string are worth knowing before you build it.

The secretKey is glued straight on, with no separator of its own. That is why every string ends in a pipe before it: …|content| + secretKey…|content|SK_abc123.

Fields you do not send still take their place — as the literal text undefined, or 0 for some numeric fields; each endpoint page notes which. Omit an optional field that belongs to the string and you have to sign that literal text, so it is easier to always send every signed field.

Not every body field is signed. extendInfo, for one, is sent but never signed. A field being in the body is not a reason to add it to the string.

Sign with RSA-SHA256 (SHA256withRSA), PKCS#1 v1.5 padding, and base64-encode the result.

JavaScript
import {createSign} from 'node:crypto';
import {readFileSync} from 'node:fs';

const privateKey = readFileSync('partner-private.pem', 'utf8');
const SECRET_KEY = process.env.ALIX_SECRET_KEY;

function sign(canonicalString) {
const signer = createSign('RSA-SHA256');
signer.update(canonicalString + SECRET_KEY); // secretKey is appended, not separated
signer.end();
return signer.sign(privateKey, 'base64');
}

// Balance: the canonical string is `partnerCode|currency|`
const body = {partnerCode: 'ACME', currency: 'USDT'};
body.signature = sign(`${body.partnerCode}|${body.currency}|`);

Verifying a response

Every successful response carries its own signature inside data, built the same way — a pipe-joined subset of the response's fields, then your secretKey — and signed with AliX's private key.

<response field 1>|…|<response field n>|<secretKey>

Note the asymmetry: on a request you append the secretKey yourself; on a response it is simply the last segment of the string you rebuild. The bytes end up the same. Verify with AliX's public key.

JavaScript
import {createVerify} from 'node:crypto';

const alixPublicKey = readFileSync('alix-public.pem', 'utf8');

function verify(canonicalString, signature) {
return createVerify('RSA-SHA256')
.update(canonicalString + SECRET_KEY)
.verify(alixPublicKey, signature, 'base64');
}

// Balance response: the canonical string is `balance|currency|secretKey`
const {data} = await res.json();
if (!verify(`${data.balance}|${data.currency}|`, data.signature)) throw new Error('bad signature');

Signature verification is a guarantee about successful responses only. An error response carries data: null, so there is nothing to verify.

Headers

Header
Content-Typeapplication/json. Required.
langAccepted and ignored. Messages are always English, whatever you send.

Verify your account

POST /api/v2/account/verify

Confirms that your partnerCode exists, your signature validates, your account is active, and your IP is allowlisted. It changes nothing, which makes it the right first call of an integration and a safe health check.

Signed string

partnerCode|<secretKey>

Request

FieldTypeRequiredNotes
partnerCodestringYes
signaturestringYesA missing signature is not rejected as a missing field; it fails signature verification, so you get errorCode 2 rather than 5.

Response data

FieldTypeNotes
namestringYour partner name.
emailstringThe account email.
statusstringAlways ACTIVE on success. A non-active partner is rejected with errorCode 3 rather than reported here.
signaturestringResponse signature over name|email|status|secretKey.
JSON
{
"success": true,
"message": "Your request has been successful",
"data": {
"name": "Acme Corp",
"email": "[email protected]",
"status": "ACTIVE",
"signature": "K3tQ...=="
},
"errorCode": 0,
"semanticCode": "SUCCESS"
}

Errors — only the shared ones: 1, 2, 3, 4, 5, 19. See Error codes.

The shared errors

Every signed endpoint runs signature verification before any business logic, so all of them can return these:

errorCodesemanticCodeWhen
1INTERNAL_SERVER_ERRORServer error — or your key material is unusable.
2INVALID_SIGNATUREThe signature does not match the canonical string, or your public key is missing.
3PARTNER_ACCOUNT_LOCKEDYour account is not active.
4PARTNER_NOT_FOUNDUnknown partnerCode.
5INVALID_REQUEST_DATARequest validation failed.
19PARTNER_IP_NOT_ALLOWEDThe calling IP is not on your allowlist.

The two public endpoints — get-qr-code-info and generate-philippines-qrcode — are not signed and return none of these.