Skip to main content

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

  1. Register your endpoint URL and its secret key in the AliX management portal, or through your account manager. Sandbox and production have separate URLs.
  2. Pass that secret as webhookSecretKey when 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:

JSON
{
"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>
JavaScript
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 200 with 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/details as 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.