Webhooks and emails
Event catalogue
Every webhook Multigate sends, when it fires, which API calls cause it, and what it contains. Each event is identified by header. (the object) and header. (what happened).
Trade finance (letters of credit and guarantees) sends no webhooks: poll its lists, and see its emails.
At a glance
notification_ / notification |
Fires when | Caused by |
|---|---|---|
transfer / transfer_ |
A transfer is paid. | Create a transfer, bulk transfer rows, remittances, approvals in the dashboard |
transfer / transfer_ |
A transfer fails at the bank or partner, or the balance is short when the payout starts. | Same as above |
transfer / refund |
A refund of an inflow completes or fails. | Refund an inflow |
account / inflow |
Money is credited to one of your accounts or a customer's. | A payer's transfer; Simulate an inflow in the sandbox |
account / account_ |
An account, or one of its virtual accounts, becomes active. | Create an account |
client / subclient_ |
Multigate finishes reviewing a customer (approved or declined). | Submit a customer for review or resubmit a declined customer |
receivables / inflow |
A payment is matched to one of your receivables (invoices you collect against). Sent together with account / inflow. |
A payer's transfer into a receivable's account |
trade / trade_ |
A conversion settles. | Lock a rate |
Events about your customers' activity are sent to your webhook URL. Use onBehalfOf, clientId or the account details in the payload to tell customers apart.
Every payload may gain new fields over time. Ignore fields you don't recognise, and don't rely on the order of fields.
What to do with each event
| Event | Find your record by | What to do |
|---|---|---|
transfer_ |
unique: the ID you sent when you created the transfer or remittance. For a bulk row, the ID Multigate generated for it. |
Mark the payout paid, unless it's already final. Store transactionId for support and reconciliation. A large NGN transfer can be split into parts, each with its own event and your ID plus -1, -2, …; the payout is fully paid when every part is completed. |
transfer_ |
unique |
Mark the payout failed and show response to your user. Any amount and fee debited are back in the source account (with responseCode 22, the balance was too low and nothing was debited; its response is the raw key insufficient today, so match on the code). To try again, create a new transfer with a new unique. |
refund |
unique: the ID you sent with the refund. |
completed: the payer has the money back. failed: no money moved, and you can try again. |
account / inflow |
transactionId: Multigate's unique ID for the payment. |
Record the credit once per transactionId. Work out whose money it is from receiving (match it to the account numbers you saved from account_). Parse amount as a decimal. Keep transactionId: you need it to refund the payment. |
account_ |
accountId (returned by Create an account) or your unique. |
Save the account numbers and bank details in funding, and show them to the people who will pay in. Add new ones; don't delete numbers missing from a later event, because events can arrive out of order. |
subclient_ |
entityId: the customer ID returned when you created the customer. |
status active: the customer is approved, and you can open accounts and pay out for it with onBehalfOf. rejected: show approval if present, fix the problem, then resubmit. |
receivables / inflow |
receivable (the receivable ID) and transaction (the payment). |
Record the payment once per transactionId, and update the invoice to receivable. |
trade_ |
id: the quote ID you locked. |
Act only on status completed, and check the target account was credited before you release money (see the warning below). |
Transfer events
sequenceDiagram
autonumber
participant You as Your backend
participant MG as Multigate
participant Bank as Bank or partner
You->>MG: Create a transfer
MG-->>You: 200, status processing
MG->>Bank: Payment
alt Paid
Bank-->>MG: Success
MG-)You: transfer / transfer_completed
else Failed
Bank-->>MG: Failure reason
MG-)You: transfer / transfer_failed
end
{
"header": { "notification_type": "transfer", "notification": "transfer_completed", "hash": "3f6c0a9e…" },
"body": {
"transactionId": "PMT-261010-093000482913",
"uniqueRequestId": "po_20261010_0001",
"status": "completed",
"responseCode": "00",
"responseMessage": "successful",
"onBehalfOf": "Ab3kP9xQ2mN7vR1sT4uV6",
"amount": 250000,
"charge": 50,
"totalAmount": 250050,
"chargesType": "",
"transferType": "NIP",
"sourceAccountNumber": "1029384756",
"sourceCurrencyIso3Code": "NGN",
"sourceCountryIso3Code": "NGA",
"sourceBankBicOrCode": "multigate",
"beneficiaryDetails": {
"accountNumber": "0123456789",
"accountName": "Ada Okafor",
"bankName": "Guaranty Trust Bank",
"bankCode": "058",
"intermediaryBank": { "bankName": "", "bankCode": "", "swiftCode": "" }
},
"approvedBy": "",
"createdAt": "2026-10-10T09:30:00.482Z",
"updatedAt": "2026-10-10T09:30:09.734Z"
}
}
Same fields, with status: "failed". response and responseCode carry the reason, often the bank's own wording.
{
"header": { "notification_type": "transfer", "notification": "transfer_failed", "hash": "8a1b2c3d…" },
"body": {
"transactionId": "PMT-261010-101544730021",
"uniqueRequestId": "po_20261010_0005",
"status": "failed",
"responseCode": "25",
"responseMessage": "Beneficiary account is dormant",
"amount": 75000,
"charge": 50,
"totalAmount": 75050
}
}
transactionId is the ID of the original inflow you refunded.
{
"header": { "notification_type": "transfer", "notification": "refund", "hash": "0f1e2d3c…" },
"body": {
"transactionId": "IN-261010-0915304412",
"uniqueRequestId": "rf_20261010_0001",
"currencyIso3Code": "NGN",
"amount": 5000,
"status": "completed",
"responseMessage": "successful",
"responseCode": "00"
}
}
| Field | Notes |
|---|---|
transactionId |
Multigate's transfer ID (PMT-…). For a refund, the ID of the original inflow. |
unique |
Your idempotency key: the unique ID you sent with the request. For parts of a split NGN transfer: yours with -1, -2, …, one event per part. |
status |
completed or failed. |
responseCode, response |
00 / successful when paid. On failure, a code and either a short machine key (such as failed) or the bank's own text. Show the text to people; don't parse it. |
amount, charge, totalAmount |
Numbers in major units. totalAmount = amount + charge. |
approvedBy |
Email of the user who approved the transfer, or empty. |
Account events
{
"header": { "notification_type": "account", "notification": "inflow", "hash": "6d5c4b3a…" },
"body": {
"amount": "150000",
"transactionId": "IN-261010-1204557781",
"transactionType": "credit",
"currencyIso3Code": "NGN",
"receivingAccountNumber": "9912345678",
"accountId": "Ac4mW8pZ2kR6tY1nQ7xVb",
"accountName": "Kola Stores Ltd",
"senderAccountName": "KOLA STORES LTD",
"senderAccountNumber": "2093847561",
"narration": "Invoice 552 payment",
"sessionId": "100004261010120455123456789012",
"paymentReference": "100004261010120455123456789012",
"bankCode": "000017",
"transactionDate": "2026-10-10T12:04:55.000Z"
}
}
amount is a decimal string with no fixed number of decimal places ("150000", "1500.5"): parse it as a decimal, never as a float. Unknown values are null or left out. Some businesses receive the legacy version, which has accountNumber instead of receiving and accountId.
The event is sent only after the inflow clears compliance screening. sender isn't sent today, and payment and sessionId carry the same bank reference. If Multigate has set up auto-sweep to a pool account for you (inflows are then credited to one central account; always the case for dynamic accounts), accountId is the pool account's ID; match payments on receiving.
A simulated inflow in the sandbox has sender set to the receiving account's own name, sender null, and a generated payment/sessionId that the simulate call doesn't return. Each payment into a receivable's account also sends this event.
De-duplicate inflows on transactionId: the same inflow can arrive as a second delivery with a new Idempotency-Key. If your business has agreed with Multigate to skip webhooks for GHS inflows, those inflows send no event; you still get the "Payment Received" email.
{
"header": { "notification_type": "account", "notification": "account_opened", "hash": "9e8d7c6b…" },
"body": {
"accountId": "Ac4mW8pZ2kR6tY1nQ7xVb",
"clientId": "Kx81mQ2pZr4tV9wYa3Bc5",
"entityId": "Kx81mQ2pZr4tV9wYa3Bc5",
"uniqueRequestId": "acct_20261010_0001",
"currencyIso3Code": "NGN",
"accountName": "Acme Payments - Kola Stores",
"status": "active",
"availableBalance": 0,
"accountType": "wallet",
"fundingDetails": [
{
"accountName": "Acme Payments - Kola Stores",
"bankOrSortCode": "000017",
"accountNumber": "9912345678",
"bankCountry": "NGA",
"bankName": "Partner Bank",
"bankAddress": null,
"countryIso3Code": "NGA",
"bic": null
}
]
}
}
funding lists the account numbers payers can send to. You may receive this event more than once for one account, as each partner bank activates. For a customer's account, clientId and entityId are the customer's ID. available is always 0 here; read balances through the API.
Customer events
client / subclient_ is sent when Multigate's compliance review of a customer finishes. Check status: active means approved, rejected means declined. Ignore approval: it doesn't reflect Multigate's decision.
The body is the customer record (the same fields as Get a customer), shortened here:
{
"header": { "notification_type": "client", "notification": "subclient_approved", "hash": "7d1e9a0c…" },
"body": {
"name": "Kola Stores Ltd",
"subClientType": "client",
"businessName": "Kola Stores Ltd",
"approvalStatus": "approved",
"entityId": "Kx81mQ2pZr4tV9wYa3Bc5",
"status": "rejected",
"createdAt": "2026-10-09T08:00:00.000Z",
"updatedAt": "2026-10-10T14:20:00.000Z",
"approvalComments": "Ownership documents could not be verified.",
"approvalDate": "Sat Oct 10 2026 14:20:00 GMT+0000 (Coordinated Universal Time)",
"parentClientId": "Ab3kP9xQ2mN7vR1sT4uV6"
}
}
approvalandComments approvalDateappear only when a reviewer decided manually. A customer approved without manual review has neither.approvalDateis free text, not ISO 8601. Use the time you received the event if you need one.- Subsidiaries are approved straight away and send no event.
Receivable and conversion events
{
"header": { "notification_type": "receivables", "notification": "inflow", "hash": "5a6b7c8d…" },
"body": {
"receivableDetails": {
"entityId": "Rc4mW8pZ2kR6tY1nQ7xVb",
"invoiceRef": "INV-2026-0042",
"amount": 500000,
"amountPaid": 500000,
"currencyIso3Code": "NGN",
"dueDate": "2026-10-31T00:00:00.000Z",
"paidDate": "2026-10-10T12:04:55.000Z",
"status": "paid"
},
"transactionDetails": {
"amount": 500000,
"transactionId": "IN-261010-1204557790",
"senderAccountName": "KOLA STORES LTD",
"transactionDate": "2026-10-10T12:04:55.000Z"
}
}
}
status is partially_, paid or overpaid, and amountPaid is the running total. Amounts are numbers. The same payment also sends account / inflow.
{
"header": { "notification_type": "trade", "notification": "trade_completed", "hash": "c4d5e6f7…" },
"body": {
"id": "FX-261010-150000482913",
"sourceAmount": "1500000",
"sourceCurrency": "NGN",
"rate": "0.000654",
"targetCurrency": "USD",
"targetAmount": "981",
"tradeReference": "FXT-261010-0007",
"status": "completed"
}
}
The event can arrive before the Lock a rate response when the conversion completes straight away: match it by id (the quote ID). Amounts and the rate are decimal strings with no fixed number of decimal places. status is completed when the event is sent at settlement; an event Multigate re-sends later carries the conversion's current status, so act only on completed.
Today trade_ is also sent when the liquidity provider reports a failed conversion. The source debit is reversed, but the event still says completed, and so does Get a conversion. Before you release money based on a conversion, check that the target account was actually credited (its statement or balance).
Full field lists are in the API reference under Webhooks › Events.
Outcomes without a webhook
These outcomes don't send a webhook today. Detect them by checking status through the API:
| Outcome | How to detect it |
|---|---|
A transfer declined by an approver in your business or in Multigate's compliance or settlement review (rejected), or stopped by compliance screening (failed, responseCode 43) |
Find transfers |
| A USD payout waiting for Multigate's operations team | Stays processing; no event until it completes or fails |
| A refund that fails because the receiving account's balance couldn't be reserved just before payment | Find transfers ?unique; status is failed |
A transfer waiting for approval (pending_) |
The create response, or Find transfers |
| Some transfer failures: a compliance stop, a Nigerian account number that isn't 10 digits, a debit that couldn't be posted | Find transfers; status is failed |
A transfer that needs manual investigation stays processing |
Contact support if it's still processing after 24 hours |
| Any event that failed all its retries, or happened while no webhook URL was registered | Not sent again; fetch the object through the API |
| An inflow that was rejected (a disabled business, or the wrong amount into a dynamic account) | Your business contact receives a "Rejected Inflow Transaction" email. Some banks refuse the payment before it reaches Multigate |
| An account that fails to open after the request | Nothing; poll Get an account by ID |
| An inflow held for compliance review | Not credited and no webhook until cleared; contact support if an expected payment is missing |
| A conversion that failed when you locked it | The Lock a rate call returns an error |
| A customer created or submitted | No event until compliance finishes reviewing |
A receivable becoming unpaid (ready) or failed |
Poll Get a receivable. If the bank opens the account after a delay, an account / account_ event with accountType receivable is sent |
| A subsidiary customer approved straight away | No event; Get a customer shows active |
| An account activated later by Multigate's approval of the bank profile behind it | Often no account_; poll accounts that stay processing |
| A beneficiary saved, a bulk batch committed | No event; each bulk row sends its own transfer event |
| A bank account linking request submitted, approved or declined | Approvers get an email on submission; check the request with Search linking requests |
| A linked account created (on approval) | No account_ event; list your accounts to find it |
| A payout from a linked NGN account waiting for funds or for bank credentials | Stays processing; check your bank balance and that credentials are uploaded |
Events that aren't sent
account/reversalis defined on the platform but isn't sent today. Use thetransfer_event, or the transfer'sfailed failedstatus, as your signal that a payout didn't happen.