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. Sandbox and production endpoints are registered separately.
  2. Pass that secret as webhookSecretKey when you create the order.

The webhook is chosen per order: the webhookSecretKey you pass at creation binds that order to the registered endpoint, and an order created without one settles silently — you read its outcome from orders/details. 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

A callback fires when an order reaches a terminal state — SUCCESS or ERROR. The create response already hands you the order object, and the states in between are readable at any time through orders/details.

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 arrives as a field in the body. Rebuild the canonical string from four fields of the payload, append your secretKey, and 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, process after
void settleOrder(o.externalOrderId, o.status);
});

The signature authenticates the order's identity and outcome — externalOrderId, type, fiatAmount, status. The other fields in the payload are not covered by it, which is one more reason the pattern below reads the full order from orders/details: treat the callback as a signed notification, and take the data from the order detail.

Delivery model

Each callback is delivered once, at settlement. Build the receiver on four habits and it stays reliable:

  • Verify, then acknowledge immediately. Return 200 with an empty body before doing any processing work; process asynchronously. What you return does not affect the order itself.
  • Make the handler idempotent on externalOrderId.
  • Confirm with orders/details. The callback tells you the moment an order settles; the order detail is the record you reconcile against.
  • Keep a reconciliation sweep. Poll any order still non-terminal past its expected settlement time, so every order reaches a terminal state in your system whether or not its callback was received.