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.
We sign our replies to you the same way, with a different key, so you can confirm a response came from us.
What we issue you
At onboarding you receive four things.
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 us the public half. It verifies your requests. |
| Our public key | Issued to you. You verify our response and webhook signatures with it. |
Your private key never leaves your server. Our key is issued per partner, so verify with the one issued to you.
We also allowlist the IPs you call from. A request from another address returns
PARTNER_IP_NOT_ALLOWED (errorCode 19). The allowlist is checked on every request.
Generating your key pair
You generate the pair yourself, and the private key stays with you. Two OpenSSL commands produce it:
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 us 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 we reject 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.
The secretKey is appended directly, with no separator of its own, so 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. If you omit an optional field that belongs to the
string, sign that literal text in its place. Sending every signed field keeps the string simple.
Only the fields printed in an endpoint's signed string are signed. extendInfo, for example, is sent
in the body but is not part of 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 our 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 our 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');
Only successful responses carry a signature. An error response has data: null.
Headers
| Header | |
|---|---|
Content-Type | application/json. Required. |
lang | Optional. Messages are returned in English. |
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.