Webhooks
When an order reaches a terminal state, AliX POSTs the whole order to your endpoint so you do not have to poll for it.
Setting one up
- Register your endpoint URL and its secret key in the AliX management portal, or through your account manager. Sandbox and production have separate URLs.
- Pass that secret as
webhookSecretKeywhen you create the order.
An order created without a webhookSecretKey never gets a callback. There is no account-wide
default and no way to add one after the fact — the webhook URL is bound to the order at creation. A
key matching no registered endpoint is rejected at creation with 13 WEBHOOK_SECRET_NOT_FOUND, and
the order is not created.
When it fires
For scan-to-pay and cashout, only terminal states fire a callback — SUCCESS and ERROR. Nothing
arrives while the order is AWAITING_PAYMENT or PROCESSING, and creating an order does not fire
one — you already have the order object in the create response.
Every order created through /api/v2 gets this v2-shaped callback. Orders created through v3 get
v3's, which is a different payload with a different signature scheme. The
choice is made per order at creation time; there is no account-wide switch.
What you receive
An HTTP POST, Content-Type: application/json, no custom headers. The body is
the order object — the same shape orders/details returns, with an
id field added:
{
"id": 184392,
"externalOrderId": "acme-co-1042",
"type": "CASHOUT",
"fiatAmount": 5015000,
"paidAmount": 5000000,
"tokenTransfer": {
"currency": "USDT",
"network": null,
"price": 26150,
"amount": 191.778,
"address": null,
"txHash": null,
"memo": null
},
"bankTransfer": {
"bankAccountName": "NGUYEN VAN A",
"bankAccountNumber": "0123456789",
"bankName": "Vietcombank",
"bankCode": "VCB",
"contentPayment": "ALIX CASHOUT 1042",
"totalPayment": 5000000,
"qrUrl": null
},
"fees": { "systemFee": 5000, "processingFee": 10000 },
"status": "SUCCESS",
"descriptions": "Success",
"createdAt": "2026-07-14 10:32:07",
"expiresAt": "2026-07-14 17:47:07",
"signature": "K3tQ...=="
}
Verify the signature
The signature is a field in the body, not a header. (v3 moved it to X-Alix-Signature; v2 never
had headers.)
Rebuild the canonical string from four fields of the payload, append your secretKey, and verify with
Verify with AliX's public key — the same one you use for response signatures:
externalOrderId|type|fiatAmount|status|<secretKey>
import express from 'express';
import {createVerify} from 'node:crypto';
import {readFileSync} from 'node:fs';
const alixPublicKey = readFileSync('alix-public.pem', 'utf8');
const SECRET_KEY = process.env.ALIX_SECRET_KEY;
const app = express();
app.post('/webhooks/alix', express.json(), (req, res) => {
const o = req.body;
const canonical = `${o.externalOrderId}|${o.type}|${o.fiatAmount}|${o.status}|${SECRET_KEY}`;
const verified = createVerify('RSA-SHA256')
.update(canonical)
.verify(alixPublicKey, o.signature, 'base64');
if (!verified) return res.sendStatus(401);
res.sendStatus(200); // acknowledge first — there is no second delivery
void settleOrder(o.externalOrderId, o.status);
});
Unlike v3, the signature covers only those four fields, not the raw body. Everything else in the
payload (the amounts, the bank details, txHash) is unsigned. So a valid signature tells you the
order id, type, total and status are authentic; it does not authenticate the rest.
So treat the callback as a notification rather than as data. When one arrives, call
POST /api/v2/orders/details and use what that returns.
Delivered once. Never retried.
One POST attempt per order. If your endpoint is down, times out, returns a 500, or the TLS handshake fails, that callback is gone. There is no retry, no backoff, no dead-letter queue, and no way to ask for a redelivery.
There is also no send timeout, so a slow endpoint holds the connection open. Do not do work inline.
Two consequences, and neither is optional:
- Acknowledge immediately, then process asynchronously. Return
200with an empty body. We do not inspect what you return — a non-2xx is logged and otherwise ignored, and it changes nothing about the order. - Poll
orders/detailsas the source of truth. Keep a reconciliation job that sweeps any order still non-terminal past its expected settlement time. The webhook is an optimisation over polling, not a replacement for it. An integration that relies on the callback alone will lose orders.
Make your handler idempotent on externalOrderId anyway.