# 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 `fundingDetails`.

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](02-accounts.md).
- A record, in your own database, of which virtual account number (`fundingDetails[].accountNumber`) belongs to which of your customers or orders. Inflows are matched on it.
- A webhook URL configured, so you receive `account` / `inflow`: see [Webhooks](../03-webhooks-and-emails/01-overview.md).

## How an inflow works

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

1. You give a payer one of the account numbers from the account's `fundingDetails` ([Accounts](02-accounts.md)).
2. The payer sends a bank transfer.
3. Multigate matches the payment to the account and holds it while compliance screening runs.
4. When screening clears, Multigate credits the wallet, then sends you `account` / `inflow` and 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 `receivingAccountNumber` (or `accountNumber` in the legacy payload).

:::info[Auto-sweep to a pool account]
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. `receivingAccountNumber` still shows the account the payer paid into, but `accountId` is the pool account's ID.
:::

## Statuses

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

```json
{
  "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. |
| `receivingAccountNumber` | 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). |
| `senderAccountName`, `senderAccountNumber`, `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. |
| `paymentReference`, `sessionId` | The bank's reference for the payment (the same value in both fields). |
| `transactionDate` | 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:

```bash
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" }'
```

See [Environments and sandbox](../01-get-started/04-environments-and-sandbox.md#simulate-an-inflow).

A simulated inflow differs from a real one:

- `senderAccountName` is the receiving account's own name, and `senderAccountNumber` is `null`.
- `paymentReference` and `sessionId` are a generated ID, which the simulate call doesn't return. Wait for the webhook to learn the `transactionId`.
- The narration looks like `MULTIGATE-<account name>-<masked account number>`.
- `currency` is ignored: the account's currency is used.

Make your handler accept `null` sender fields.

:::danger[Production ignores simulated inflows]
In production, the simulate endpoint still answers `200` "inflow initiated" but does nothing. Keep it out of your production code. See [Sandbox vs production](../01-get-started/05-sandbox-vs-production.md#inflows-and-virtual-accounts).
:::

## 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](02-accounts.md#dynamic-accounts). 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](02-accounts.md)). Store each of its `fundingDetails[].accountNumber` against the customer. | `receivingAccountNumber` → your customer |
| One order or invoice, exact amount | Open a [dynamic account](02-accounts.md#dynamic-accounts) per order. Store its `fundingDetails.accountNumber` against the order. | `receivingAccountNumber` → your order |
| One invoice, with part-payments and overpayments tracked | Use [Receivables](11-receivables.md): 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:

1. Verify the signature, and skip it if you've already recorded this `transactionId`.
2. Look up `receivingAccountNumber` (legacy payload: `accountNumber`) in your records to find the customer or order.
3. Record `transactionId`, `amount` (as a decimal) and `currencyIso3Code` against it, and credit your customer in your own system.
4. 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](08-refunds.md), using its `transactionId`.

## Reconciliation

Each day, compare the inflows you recorded with the account's statement: [Get an account statement](api:getAccountStatement) 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](10-balances-and-statements.md).

## Next

<CardGroup cols={2}>
<Card title="Accounts" icon="lucide-wallet" href="02-accounts.md">
Open the accounts payers pay into.
</Card>
<Card title="Collections for platforms" icon="lucide-store" href="../05-use-cases/02-collections-for-platforms.md">
One account per merchant, end to end.
</Card>
</CardGroup>
