# Event catalogue

Every webhook Multigate sends, when it fires, which API calls cause it, and what it contains. Each event is identified by `header.notification_type` (the object) and `header.notification` (what happened).

[Trade finance](../04-products/12-trade-finance.md) (letters of credit and guarantees) sends no webhooks: poll its lists, and see its [emails](04-email-notifications.md#trade-finance).

## At a glance

| `notification_type` / `notification` | Fires when | Caused by |
|---|---|---|
| `transfer` / `transfer_completed` | A transfer is paid. | [Create a transfer](api:createSingleTransfer), [bulk transfer](api:commitBulkTransfer) rows, [remittances](api:createRemittance), approvals in the dashboard |
| `transfer` / `transfer_failed` | 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](api:refundInflow) |
| `account` / `inflow` | Money is credited to one of your accounts or a customer's. | A payer's transfer; [Simulate an inflow](api:simulateInflow) in the sandbox |
| `account` / `account_opened` | An account, or one of its virtual accounts, becomes active. | [Create an account](api:createAccount) |
| `client` / `subclient_approved` | Multigate finishes reviewing a customer (approved **or** declined). | [Submit a customer for review](api:submitSubClientForApproval) or [resubmit a declined customer](api:resubmitSubClient) |
| `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_completed` | A conversion settles. | [Lock a rate](api:lockFxRate) |

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_completed` | `uniqueRequestId`: the ID you sent when you created the transfer or remittance. For a [bulk](../04-products/06-bulk-payouts.md) 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](../04-products/05-payouts.md#large-ngn-transfers-are-split), each with its own event and your ID plus `-1`, `-2`, …; the payout is fully paid when every part is `completed`. |
| `transfer_failed` | `uniqueRequestId` | Mark the payout failed and show `responseMessage` 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 `responseMessage` is the raw key `insufficientBalance` today, so match on the code). To try again, create a new transfer with a **new** `uniqueRequestId`. |
| `refund` | `uniqueRequestId`: the ID you sent with the refund. | `completed`: the payer has the money back. `failed`: no money moved, and you can [try again](../04-products/08-refunds.md). |
| `account` / `inflow` | `transactionId`: Multigate's unique ID for the payment. | Record the credit once per `transactionId`. Work out whose money it is from `receivingAccountNumber` (match it to the account numbers you saved from `account_opened`). Parse `amount` as a decimal. Keep `transactionId`: you need it to [refund the payment](api:refundInflow). |
| `account_opened` | `accountId` (returned by Create an account) or your `uniqueRequestId`. | Save the account numbers and bank details in `fundingDetails`, 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_approved` | `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 `approvalComments` if present, fix the problem, then [resubmit](api:resubmitSubClient). |
| `receivables` / `inflow` | `receivableDetails.entityId` (the receivable ID) and `transactionDetails.transactionId` (the payment). | Record the payment once per `transactionId`, and update the invoice to `receivableDetails.status`. |
| `trade_completed` | `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

```mermaid
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
```

<Tabs>
<Tab title="transfer_completed">

```json
{
  "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"
  }
}
```

</Tab>
<Tab title="transfer_failed">

Same fields, with `status: "failed"`. `responseMessage` and `responseCode` carry the reason, often the bank's own wording.

```json
{
  "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
  }
}
```

</Tab>
<Tab title="refund">

`transactionId` is the ID of the **original inflow** you refunded.

```json
{
  "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"
  }
}
```

</Tab>
</Tabs>

| Field | Notes |
|---|---|
| `transactionId` | Multigate's transfer ID (`PMT-…`). For a refund, the ID of the original inflow. |
| `uniqueRequestId` | 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`, `responseMessage` | `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

<Tabs>
<Tab title="inflow">

```json
{
  "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 `receivingAccountNumber` and `accountId`.

The event is sent only after the inflow clears compliance screening. `senderBankName` isn't sent today, and `paymentReference` and `sessionId` carry the same bank reference. If Multigate has set up [auto-sweep to a pool account](../04-products/03-collections.md#how-an-inflow-works) for you (inflows are then credited to one central account; always the case for [dynamic accounts](../04-products/02-accounts.md#dynamic-accounts)), `accountId` is the pool account's ID; match payments on `receivingAccountNumber`.

A [simulated inflow](api:simulateInflow) in the sandbox has `senderAccountName` set to the receiving account's own name, `senderAccountNumber` `null`, and a generated `paymentReference`/`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.

</Tab>
<Tab title="account_opened">

```json
{
  "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
      }
    ]
  }
}
```

`fundingDetails` 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. `availableBalance` is always `0` here; read balances through the API.

</Tab>
</Tabs>

## Customer events

`client` / `subclient_approved` is sent when Multigate's compliance review of a customer finishes. **Check `status`**: `active` means approved, `rejected` means declined. Ignore `approvalStatus`: it doesn't reflect Multigate's decision.

The body is the customer record (the same fields as [Get a customer](api:getSubClient)), shortened here:

```json
{
  "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"
  }
}
```

- `approvalComments` and `approvalDate` appear only when a reviewer decided manually. A customer approved without manual review has neither.
- `approvalDate` is 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

<Tabs>
<Tab title="receivables / inflow">

```json
{
  "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`, `paid` or `overpaid`, and `amountPaid` is the running total. Amounts are numbers. The same payment also sends `account` / `inflow`.

</Tab>
<Tab title="trade / trade_completed">

```json
{
  "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](api:lockFxRate) 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`.

:::warning[Confirm conversions before releasing funds]
Today `trade_completed` 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](api:getFxTrade). Before you release money based on a conversion, check that the target account was actually credited (its statement or balance).
:::

</Tab>
</Tabs>

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](api:listTransfers) |
| 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](api:listTransfers) `?uniqueRequestId=`; `status` is `failed` |
| A transfer waiting for approval (`pending_approval`) | The create response, or [Find transfers](api:listTransfers) |
| Some transfer failures: a compliance stop, a Nigerian account number that isn't 10 digits, a debit that couldn't be posted | [Find transfers](api:listTransfers); `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](api:getAccountById) |
| 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](api:lockFxRate) 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](api:getReceivable). If the bank opens the account after a delay, an `account` / `account_opened` event with `accountType` `receivable` is sent |
| A subsidiary customer approved straight away | No event; [Get a customer](api:getSubClient) shows `active` |
| An account activated later by Multigate's approval of the bank profile behind it | Often no `account_opened`; 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](api:searchAccountLinking) |
| A linked account created (on approval) | No `account_opened` 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` / `reversal` is defined on the platform but **isn't sent** today. Use the `transfer_failed` event, or the transfer's `failed` status, as your signal that a payout didn't happen.
