Authentication
There is no login in v2, and no token. 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, from the management portal or your account manager.
partnerCode | Identifies you. It goes in the body of nearly every request. |
secretKey | A shared secret. It is never transmitted; it is the tail of every signed string. |
| Your public key | You generate an RSA-2048 key pair and give AliX the public half. It verifies your requests. |
| AliX's public key | Issued 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 use the one you were given, rather than a key from someone else's integration or from an old document.
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.
Signing a request
The signed string is not the JSON body. It is a fixed, ordered, pipe-joined subset of the request's
fields, 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. You cannot derive it from
the body, so every endpoint page prints its exact string. Put one field out of order and you get
INVALID_SIGNATURE (errorCode 2), which will not tell you which field it was.
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, and undefined is interpolated literally. If you omit
an optional field that belongs to the string, you have to sign that literal text — so it is easier to
always send it.
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.
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>
The asymmetry is easy to get backwards. 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.
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');
Error responses are not signed. They carry data: null, so there is nothing to verify — signature
verification is a guarantee about successful responses only.
Headers
| Header | |
|---|---|
Content-Type | application/json. Required. |
lang | Accepted and ignored. v2 messages are always English, whatever you send. |
There is no Authorization header in v2, and no bearer token.
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
| Field | Type | Required | Notes |
|---|---|---|---|
partnerCode | string | Yes | |
signature | string | Yes | A missing signature is not rejected as a missing field; it fails signature verification, so you get errorCode 2 rather than 5. |
Response data
| Field | Type | Notes |
|---|---|---|
name | string | Your partner name. |
email | string | The account email. |
status | string | Always ACTIVE on success. A non-active partner is rejected with errorCode 3 rather than reported here. |
signature | string | Response signature over name|email|status|secretKey. |
{
"success": true,
"message": "Your request has been successful",
"data": {
"name": "Acme Corp",
"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:
errorCode | semanticCode | When |
|---|---|---|
1 | INTERNAL_SERVER_ERROR | Server error — or your key material is unusable. |
2 | INVALID_SIGNATURE | The signature does not match the canonical string, or your public key is missing. |
3 | PARTNER_ACCOUNT_LOCKED | Your account is not active. |
4 | PARTNER_NOT_FOUND | Unknown partnerCode. |
5 | INVALID_REQUEST_DATA | Request validation failed. |
19 | PARTNER_IP_NOT_ALLOWED | The 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.