Webhook events
Every event MigraPay sends, the request it arrives in, and every field.
MigraPay sends an HTTPS POST to your endpoints when a payout or collection changes. To set up an endpoint and verify deliveries, see the Webhooks guide.
The request
POST /webhooks/migrapay HTTP/1.1
Content-Type: application/json
X-MigraPay-Signature: 5d0b6f0a3c1e9b7d2f48a6c5e1d3b9f07a2c4e6d8b0f1a3c5e7d9b1f3a5c7e9d
X-MigraPay-Timestamp: 1786543691
X-MigraPay-Signature-V2: 9a4e1c7b2d5f8e0a3c6b9d2f5e8a1c4b7d0e3f6a9c2b5e8d1f4a7c0b3e6d9f2a
X-MigraPay-Event: payment.settled
X-MigraPay-Mode: live
X-MigraPay-Delivery: 0c7d4e2f-9a1b-4c3d-8e5f-000000000001
X-MigraPay-Request-Id: 7e2a9c4b-1d3f-4a5e-b6c8-000000000001
{"id":"evt_6178cec07838ef6eff03321e","event":"payment.settled","mode":"live","createdAt":"2026-08-12T16:08:11.204Z","data":{"transactionId":"a41c7e92-3b5d-4f08-8c6e-000000000001","reference":"20260812160811-K7Q2M9X4TD","currency":"CAD","amount":250,"toCurrency":"NGN","toAmount":271050}}| Header | Description |
|---|---|
X-MigraPay-Signature-V2 | Hex-encoded HMAC-SHA256 of {timestamp}.{raw body}, keyed with your signing secret. Use this one. |
X-MigraPay-Timestamp | When the delivery was signed, in Unix seconds. Covered by the V2 signature. |
X-MigraPay-Signature | Hex-encoded HMAC-SHA256 of the raw body alone. Kept for existing integrations. |
X-MigraPay-Event | The event name, same as event in the body. |
X-MigraPay-Mode | live or sandbox, same as mode in the body. |
X-MigraPay-Delivery | The delivery id. Stays the same across retries, so use it to ignore duplicates. |
X-MigraPay-Request-Id | Unique per attempt. Useful when matching an attempt to your logs. |
The body is always:
{ "id": "evt_…", "event": "<name>", "mode": "live", "createdAt": "<ISO 8601>", "data": { … } }id identifies the event and is the same on every endpoint and every retry. mode is live or sandbox: Sandbox events are sent exactly like live ones, with the same fields, and differ only in mode, so check it before you act on money.
Events
| Event | Sent when | data |
|---|---|---|
payment.settled | A payout has been delivered. | transactionId, reference, currency, amount, toCurrency, toAmount, rail, recipientId, narration |
payment.failed | A payout could not be delivered. Your wallet has been credited back. | transactionId, reference, reason (failed), currency, amount, toCurrency, toAmount, rail, recipientId, narration |
payment.reversed | A delivered payout was returned. Your wallet has been credited back. | transactionId, reference, reason (returned_by_bank), currency, amount, toCurrency, toAmount, rail, recipientId, narration |
collection.settled | A customer paid a collection and the funds are in your wallet. | requestId, gross, fee, net, currency, reference, description, rail, paymentCode, payer, message, sourceAccount, amountReceived, mismatch |
collection.refunded | All or part of a settled collection was refunded to the payer. | requestId, refundId, amount, gross, refundedTotal, partial, currency |
collection.expired | A collection or checkout was not paid before it expired. | requestId, reference, currency, amount, rail, checkout |
Payout events use transactionId and reference from the payout. Collection events use requestId, the collection id. amount is in the wallet currency; toAmount is what the recipient received. In rare cases where a payout settles after it was already returned to your wallet, payment.settled arrives without toCurrency and toAmount.
Fields that do not apply to an event are left out of data rather than sent as null. New fields may be added at any time, so ignore keys you do not know.
Payout fields
| Field | Description |
|---|---|
rail | How the payout was sent: bank, interac, mobile_money, crypto, … |
recipientId | The recipient it was sent to. |
narration | The description you gave the payout. |
currency, amount | What left your wallet. Also on payment.failed and payment.reversed, where it is the amount credited back. |
toCurrency, toAmount | What the recipient receives (or would have received), after conversion. |
Collection fields
| Field | Description |
|---|---|
gross, fee, net | What the payer sent, our fee, and what was credited to your wallet. |
reference, description | Your own reference and description from the collection. Self-deposits made with your Interac business code carry the business code as reference and Interac deposit as description; deposits to a standing NGN account carry Bank deposit. |
rail | interac, virtual_account, dynamic_virtual_account, … |
paymentCode | Interac only: the collection's payment code (for example ACME7K-1001). |
payer | { name, email } of who paid, as seen on the payment. Either may be null. |
message | Interac only: the message the payer wrote on the e-Transfer. |
sourceAccount | NGN account transfers: { name, bank, accountNumberMasked } of the account the money came from. |
amountReceived | The amount that actually arrived (same as gross). |
mismatch | true when amountReceived differs from the amount the collection asked for. The collection settles for what arrived and is flagged for review; see Interac e-Transfer. Absent otherwise. |
paymentMethod | How the customer paid: card, bank_transfer, ussd, interac, virtual_account, ach, eft or wire. |
funding | How the money arrived, the same shape for every method. See The funding object. |
metadata | The metadata you sent when you created the collection or checkout, returned exactly as you sent it (null if you sent none). Payment links add paymentLinkId and slug; checkout widgets add widgetId. Also on collection.refunded and collection.expired. |
checkout | Checkouts only: { base: { currency, amount }, metadata, method }. base is the price you set (the customer may have paid in another currency); metadata is what you sent; method is how they paid (card, bank_transfer, ussd, interac). |
The funding object
Every collection.settled carries funding, built the same way whichever bank, card network or rail the money came through:
| Field | Description |
|---|---|
method | Same as paymentMethod. |
sessionId | The bank network's trace for the transfer: the 30-digit NIP session id for Nigerian transfers and USSD, the Interac reference number for e-Transfers. null for cards. |
reference | The payment reference on the sender's side. |
narration | The narration or message on the transfer, when the bank sends one. |
sender | { name, bankName, accountNumber } of the account the money came from. Fields the bank does not report are null; sender is null for cards. |
card | Cards only: { brand, last4 }, each null when the card network does not report it. null for every other method. |
receivedAt | When the money arrived (ISO 8601, UTC). |
A Nigerian bank transfer:
"funding": {
"method": "bank_transfer",
"sessionId": "000013260621182330000013203297",
"reference": "000013203297",
"narration": "NIP Transfer to Naija Market Day. 000013260621182330000013203297",
"sender": { "name": "OKOYE CHIZOBA ANTHONY", "bankName": "GT Bank", "accountNumber": "0697122703" },
"card": null,
"receivedAt": "2026-06-21T17:23:37.653Z"
}A card payment:
"funding": {
"method": "card",
"sessionId": null,
"reference": "MP-58D492ECD829",
"narration": null,
"sender": null,
"card": { "brand": null, "last4": null },
"receivedAt": "2026-11-03T15:08:00.927Z"
}An Interac e-Transfer:
"funding": {
"method": "interac",
"sessionId": "CA1MRdK8yG7x",
"reference": "ACME7K-1001",
"narration": "Invoice 1048",
"sender": { "name": "Jordan Smith", "bankName": null, "accountNumber": null },
"card": null,
"receivedAt": "2026-08-12T16:08:11.000Z"
}On collection.refunded, amount (and gross, kept for older integrations) is the amount of this refund; refundedTotal is the total refunded so far, and partial is true while part of the collection is still unrefunded.
Sample payloads
Download sample webhooks (JSON): one realistic delivery per scenario, with the exact headers and body we send. It covers collection.settled for a CAD Interac collection paid with its payment code, a self-deposit with your business code, an amount mismatch, a standing NGN account and a one-time NGN account; a partial collection.refunded; payment.settled for a CAD Interac payout and for a CAD to NGN bank payout; payment.failed; and payment.reversed.
Each entry is { "headers": { … }, "body": { "id": …, "event": …, "mode": …, "createdAt": …, "data": { … } } }. The ids and amounts are made up, but the signatures are real: they are computed with the signing secret whsec_3f9a1c7e5b2d8f4a6c0e9b1d7f3a5c8e over JSON.stringify(body), so you can replay a sample against your endpoint with that secret to test your verification. Their timestamps are in the past, so a correct verifier rejects them as stale: relax the age check while you test, never in production.