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}}
HeaderDescription
X-MigraPay-Signature-V2Hex-encoded HMAC-SHA256 of {timestamp}.{raw body}, keyed with your signing secret. Use this one.
X-MigraPay-TimestampWhen the delivery was signed, in Unix seconds. Covered by the V2 signature.
X-MigraPay-SignatureHex-encoded HMAC-SHA256 of the raw body alone. Kept for existing integrations.
X-MigraPay-EventThe event name, same as event in the body.
X-MigraPay-Modelive or sandbox, same as mode in the body.
X-MigraPay-DeliveryThe delivery id. Stays the same across retries, so use it to ignore duplicates.
X-MigraPay-Request-IdUnique 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

EventSent whendata
payment.settledA payout has been delivered.transactionId, reference, currency, amount, toCurrency, toAmount, rail, recipientId, narration
payment.failedA payout could not be delivered. Your wallet has been credited back.transactionId, reference, reason (failed), currency, amount, toCurrency, toAmount, rail, recipientId, narration
payment.reversedA delivered payout was returned. Your wallet has been credited back.transactionId, reference, reason (returned_by_bank), currency, amount, toCurrency, toAmount, rail, recipientId, narration
collection.settledA 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.refundedAll or part of a settled collection was refunded to the payer.requestId, refundId, amount, gross, refundedTotal, partial, currency
collection.expiredA 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

FieldDescription
railHow the payout was sent: bank, interac, mobile_money, crypto, …
recipientIdThe recipient it was sent to.
narrationThe description you gave the payout.
currency, amountWhat left your wallet. Also on payment.failed and payment.reversed, where it is the amount credited back.
toCurrency, toAmountWhat the recipient receives (or would have received), after conversion.

Collection fields

FieldDescription
gross, fee, netWhat the payer sent, our fee, and what was credited to your wallet.
reference, descriptionYour 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.
railinterac, virtual_account, dynamic_virtual_account, …
paymentCodeInterac 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.
messageInterac only: the message the payer wrote on the e-Transfer.
sourceAccountNGN account transfers: { name, bank, accountNumberMasked } of the account the money came from.
amountReceivedThe amount that actually arrived (same as gross).
mismatchtrue 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.
paymentMethodHow the customer paid: card, bank_transfer, ussd, interac, virtual_account, ach, eft or wire.
fundingHow the money arrived, the same shape for every method. See The funding object.
metadataThe 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.
checkoutCheckouts 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:

FieldDescription
methodSame as paymentMethod.
sessionIdThe 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.
referenceThe payment reference on the sender's side.
narrationThe 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.
cardCards only: { brand, last4 }, each null when the card network does not report it. null for every other method.
receivedAtWhen 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.