Webhooks
Configure an HTTPS webhook URL from the Merchant Dashboard. Importapay signs each delivery with your merchant webhook secret and sends the signature in x-importapay-signature.
Delivery behaviour
Return any 2xx response to acknowledge a delivery. Timeouts, 429 responses, and all other non-2xx responses are retried with exponential backoff, for up to eight delivery attempts. Retries retain the same eventId.
Store the eventId before processing the event, and make processing idempotent. A webhook must never create a duplicate business effect when a delivery is retried.
Events
| Event | Meaning |
|---|---|
virtual_account.payment_received | An exact customer credit has been confirmed for the dynamic account. |
virtual_account.settled | The delayed sweep and reconciliation have completed. |
virtual_account.expired | No valid payment was recorded before the account expired. |
virtual_account.failed | Reconciliation failed, such as because of an amount mismatch or late payment. |
Headers and signature verification
Each delivery includes these headers:
| Header | Purpose |
|---|---|
x-importapay-signature | Hex-encoded HMAC-SHA256 of the exact JSON request body, signed with your webhook secret. |
x-importapay-event-id | The same stable identifier provided in the body as eventId. |
Verify the signature against the raw request bytes before parsing or processing JSON. For example, in Node.js:
import crypto from "node:crypto";
const expectedSignature = crypto
.createHmac("sha256", process.env.IMPORTAPAY_WEBHOOK_SECRET!)
.update(rawRequestBody)
.digest("hex");
const receivedSignature = request.headers["x-importapay-signature"];
const isValid =
typeof receivedSignature === "string" &&
receivedSignature.length === expectedSignature.length &&
crypto.timingSafeEqual(
Buffer.from(receivedSignature, "hex"),
Buffer.from(expectedSignature, "hex"),
);
Payload example
{
"eventId": "dynamic-account:66c9b4b736d142b900000001:virtual_account.payment_received",
"event": "virtual_account.payment_received",
"createdAt": "2026-08-25T12:00:00.000Z",
"data": {
"dynamicAccountId": "66c9b4b736d142b900000001",
"accountNumber": "0123456789",
"accountName": "Order INV-1001",
"bankName": "Aella Microfinance Bank",
"currency": "NGN",
"expectedAmount": 50000,
"status": "paid",
"expiresAt": "2026-08-25T12:30:00.000Z",
"paidAt": "2026-08-25T12:00:00.000Z",
"providerTransactionId": "aella_tx_123"
}
}
The virtual_account.failed payload adds data.failureReason. Dynamic-account webhook payloads deliberately exclude blockchain transaction hashes and intermediate mint details.