Last updated
There are 2 ways to get notified of order status changes via webhooks:
Global webhook URL set in the merchant dashboard (applies to all orders)
Per-order webhookUrl field when creating an order (overrides global URL for that order)
You can read how to set a global webhook endpoint HERE
When an order status changes, a POST request is sent to the webhook URL with the following payload type:
Example payload
We send a signature with each webhook request to protect merchants from fraudulent requests. Each request should be verified using a secret provided in the merchant dashboard.
The signature is sent in the x-signature HTTP header.
The signature is computed as follows:
TypeScript example:
Your webhook endpoint must:
Respond with HTTP status code 2xx to acknowledge receipt
Respond within 20 seconds (requests taking longer will timeout)
Any other status code or timeout will be considered a failure.
If your webhook endpoint fails to respond successfully:
We will retry up to 10 times
Retries will be attempted with exponential backoff: 1sec, 2sec, 4sec, 8sec, 16sec, 32sec, 64sec, 128sec, 256sec, 512sec
Webhooks can be viewed in the merchant dashboard
Last updated
type Webhook = {
event: 'order-status-change';
data: {
order: {
userId: string;
userEmail: string;
merchantOrderParams?: string;
countryIsoCode: string;
flow: FlowType;
type: OrderType;
source: Source;
status: OrderStatus;
deposit: {
paymentChannel: PaymentChannel;
currencyType: CurrencyType;
currencyCode: string;
cashout: {
exchangeRate: number;
exchangeRateAfterFees: number;
amountBeforeFees: number;
amountAfterFees: number;
amountBeforeFeesUsd: number;
amountAfterFeesUsd: number;
};
};
payout: {
paymentChannel: PaymentChannel;
currencyType: CurrencyType;
currencyCode: string;
cashout: {
exchangeRate: number;
exchangeRateAfterFees: number;
amountBeforeFees: number;
amountAfterFees: number;
amountBeforeFeesUsd: number;
amountAfterFeesUsd: number;
};
transaction?: {
meta?: {
transactionHash?: string;
};
};
};
refund?: {
paymentChannel: PaymentChannel;
currencyType: CurrencyType;
currencyCode: string;
cashout: {
exchangeRate: number;
exchangeRateAfterFees: number;
amountBeforeFees: number;
amountAfterFees: number;
amountBeforeFeesUsd: number;
amountAfterFeesUsd: number;
};
transaction?: {
meta?: {
transactionHash?: string;
};
};
};
createdAt: Date;
updatedAt: Date;
};
userKyc?: {
passedKycType?: KycType;
passedKycHash?: string; // unique KYC submission identifier
latestKycType?: KycType;
latestKycStatus?: KycStatus;
};
};
}{
"event": "order-status-change",
"data": {
"order": {
"userId": "68df8fcb372f378356ef7568",
"userEmail": "chauncey69@gmail.com",
"merchantOrderParams": "01K6MMKBKC8CX4SMJAR49DX5RZ",
"countryIsoCode": "NG",
"flow": "regular",
"type": "on_ramp",
"source": "api",
"status": "payout_successful",
"deposit": {
"paymentChannel": "bank",
"currencyType": "fiat",
"currencyCode": "NGN",
"cashout": {
"exchangeRate": 1460.2,
"exchangeRateAfterFees": 1505.2969,
"amountBeforeFees": 15054,
"amountAfterFees": 14603,
"amountBeforeFeesUsd": 10.309547,
"amountAfterFeesUsd": 10.000685
}
},
"payout": {
"paymentChannel": "merchant_balance",
"currencyType": "merchant_balance",
"currencyCode": "USD",
"cashout": {
"exchangeRate": 1,
"exchangeRateAfterFees": 1,
"amountBeforeFees": 10,
"amountAfterFees": 10,
"amountBeforeFeesUsd": 10,
"amountAfterFeesUsd": 10
}
},
"createdAt": "2025-10-03T08:56:43.212Z",
"updatedAt": "2025-10-03T08:57:03.247Z"
}
}
}x-signature === SHA256(SHA256(secret) + JSON.stringify(request.body))import { createHash } from 'crypto';
function verifyWebhookSignature(
requestBody: any,
signature: string,
secret: string
): boolean {
const expectedSignature = createHash('sha256')
.update(JSON.stringify(requestBody))
.update(createHash('sha256').update(secret, 'utf8').digest('hex'))
.digest('hex');
return signature === expectedSignature;
}
