Products
Collections
Collect local-currency payments into named virtual accounts, yours or your customers', and know when money arrives. Payers pay by bank transfer to an account number in the account's funding.
An inflow is money received into one of your accounts. A virtual account is a bank account number at a partner bank that credits your wallet (your balance on Multigate).
Before you start
- An active wallet with at least one virtual account: see Accounts.
- A record, in your own database, of which virtual account number (
funding) belongs to which of your customers or orders. Inflows are matched on it.Details[]. account Number - A webhook URL configured, so you receive
account/inflow: see Webhooks.
How an inflow works
sequenceDiagram
autonumber
participant Payer
participant Bank as Partner bank
participant MG as Multigate
participant You as Your backend
Payer->>Bank: Bank transfer to the virtual account number
Bank->>MG: Payment notification
Note over MG: Match to the account, hold the money for screening
Note over MG: Screening clears: credit the wallet (or your pool account)
MG-)You: Webhook account / inflow
MG-->>You: Email "Payment Received" to your business contact
- You give a payer one of the account numbers from the account's
funding(Accounts).Details - The payer sends a bank transfer.
- Multigate matches the payment to the account and holds it while compliance screening runs.
- When screening clears, Multigate credits the wallet, then sends you
account/inflowand a "Payment Received" email. Nothing is sent before the credit.
Inflows into your customers' accounts come to your webhook URL and your business contact email. The payload has no customer ID: identify the account from receiving (or accountNumber in the legacy payload).
Auto-sweep means each inflow is moved automatically into one pool account (a single wallet that collects for many virtual accounts). If Multigate has set this up for you (always the case for dynamic accounts), inflows are credited to the pool account instead of the receiving wallet. receiving still shows the account the payer paid into, but accountId is the pool account's ID.
Statuses
stateDiagram-v2
direction LR
[*] --> screening: payment received
screening --> credited: screening clears (webhook + email)
screening --> held: needs review or flagged
held --> credited: reviewer clears it
[*] --> rejected: disabled account, or wrong amount on a dynamic account
You only see the credited outcome. Held and rejected inflows send no webhook.
The inflow webhook
{
"header": { "notification_type": "account", "notification": "inflow", "hash": "6d5c4b3a…" },
"body": {
"amount": "150000",
"paymentReference": "100004261010120455123456789012",
"transactionId": "IN-261010-1204557781",
"sessionId": "100004261010120455123456789012",
"transactionType": "credit",
"accountName": "Kola Stores Ltd",
"bankCode": "000017",
"senderAccountName": "KOLA STORES LTD",
"senderAccountNumber": "2093847561",
"currencyIso3Code": "NGN",
"narration": "Invoice 552 payment",
"transactionDate": "2026-10-10T12:04:55.000Z",
"receivingAccountNumber": "9912345678",
"accountId": "Ac4mW8pZ2kR6tY1nQ7xVb"
}
}
| Field | Use it for |
|---|---|
transactionId |
Multigate's ID for the inflow (IN-…). Store it. You need it exactly to refund the inflow, and it is the line id in statements. |
amount |
A decimal string without fixed decimals ("150000", "1500.5"). Parse it as a decimal. |
receiving |
The virtual account the payer paid into. Map it to your merchant or customer. |
accountId |
The wallet credited (the pool account when auto-sweep applies). |
accountName, bankCode |
The receiving virtual account's name and its partner bank's code (not the payer's bank). |
sender, sender, narration |
Who paid and why. Use the narration to match payments to orders. Any of them can be null: Multigate passes on what the payer's bank sends and leaves a field empty rather than guess it. Some banks send placeholders instead of a value, so treat values like NIL as missing too. |
payment, sessionId |
The bank's reference for the payment (the same value in both fields). |
transaction |
When Multigate recorded the payment, not the bank's time. |
The payer's bank name isn't sent.
Process it like every webhook: verify the signature, store the Idempotency-Key, and also check you haven't already recorded this transactionId. Multigate retries a delivery your server didn't acknowledge with a 2xx (up to 5 retries, 5 to 10 minutes apart), so the same inflow can arrive more than once.
Test it in the sandbox
Simulate a payment into any active account:
curl -X POST "$MG_BASE_URL/client-service/api/v1/webHook/publishInflow" \
-H "mp-api-key: $MG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountNumber": "9912345678", "amount": 150000, "currency": "NGN" }'
A simulated inflow differs from a real one:
senderis the receiving account's own name, andAccount Name senderisAccount Number null.paymentandReference sessionIdare a generated ID, which the simulate call doesn't return. Wait for the webhook to learn thetransactionId.- The narration looks like
MULTIGATE-<account name>-<masked account number>. currencyis ignored: the account's currency is used.
Make your handler accept null sender fields.
In production, the simulate endpoint still answers 200 "inflow initiated" but does nothing. Keep it out of your production code. See Sandbox vs production.
Inflows held for review
Every inflow is screened before it is credited. Until then the money is held on Multigate's side and isn't in your balance. Screening checks the payer's name against sanctions lists, and the payer's name and narration for other compliance risks. It also notes whether the payer is the account holder itself (for example, a business funding its own wallet from its own bank account) or someone else.
An inflow that needs a closer look waits for Multigate's compliance team. The most common reason is that the payer's bank sent no sender name, so the payer can't be screened automatically. An inflow that screening flags stays held. No webhook or email is sent while an inflow is held, and none is sent if it stays held. If a payer says they've paid and nothing has arrived after a reasonable time, contact support with the amount, date, receiving account and sender details.
Rejected inflows
Some incoming payments are rejected instead of credited:
- A payment into an account whose business or customer is disabled.
- A payment of the wrong amount into a dynamic account. Some banks refuse it before it reaches Multigate.
No webhook is sent for a rejected inflow. When Multigate rejects it, your business contact receives a "Rejected Inflow Transaction" email (its text always says the account is disabled). Contact support to return the money to the payer.
Match an inflow to your customer or order
The inflow webhook has no customer ID and no order ID. You match it through the account number the payer paid into, so choose the pattern before you hand out account numbers:
| You want to match each payment to | Do this | Match on |
|---|---|---|
| A customer (merchant, business client) | Open one account per customer with onBehalfOf (Accounts). Store each of its funding against the customer. |
receiving → your customer |
| One order or invoice, exact amount | Open a dynamic account per order. Store its funding against the order. |
receiving → your order |
| One invoice, with part-payments and overpayments tracked | Use Receivables: each invoice gets its own account, and you also receive receivables / inflow with the invoice status. |
The receivable |
| Several orders paid into one shared account | Ask payers to put your order number in the transfer narration. | narration (best effort: payers mistype it, and it can be null) |
Then, for each account / inflow webhook:
- Verify the signature, and skip it if you've already recorded this
transactionId. - Look up
receiving(legacy payload:Account Number accountNumber) in your records to find the customer or order. - Record
transactionId,amount(as a decimal) andcurrencyagainst it, and credit your customer in your own system.Iso3 Code - If no record matches, keep the inflow in an "unmatched" queue for your operations team. Don't drop it.
Don't match on accountId when auto-sweep is set up: it's the pool account, shared by many virtual accounts.
Sending money back
Refund an inflow to its sender with Refunds, using its transactionId.
Reconciliation
Each day, compare the inflows you recorded with the account's statement: Get an account statement lists every credit (line id = the webhook's transactionId) with a running balance. A collection fee, if your agreement has one, is a separate Fee debit line (on a dedicated fee account instead, if Multigate has set one up for you). An inflow in the statement that you didn't record usually means a missed webhook. See Balances and statements.
Next
Open the accounts payers pay into.
One account per merchant, end to end.