Skip to main content

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

EventMeaning
virtual_account.payment_receivedAn exact customer credit has been confirmed for the dynamic account.
virtual_account.settledThe delayed sweep and reconciliation have completed.
virtual_account.expiredNo valid payment was recorded before the account expired.
virtual_account.failedReconciliation failed, such as because of an amount mismatch or late payment.

Headers and signature verification

Each delivery includes these headers:

HeaderPurpose
x-importapay-signatureHex-encoded HMAC-SHA256 of the exact JSON request body, signed with your webhook secret.
x-importapay-event-idThe 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.