# Collections for fintechs and platforms

Give each of your merchants their own named NGN account number, know as soon as their buyers pay, and pay merchants out to their own banks.

**Who it's for:** fintechs, marketplaces and platforms that collect money on behalf of the businesses they serve. In these docs your merchants are **customers** (the API calls them *sub-clients*): businesses you onboard and act for with `onBehalfOf`.

## Architecture

```mermaid
flowchart LR
    subgraph mg [Multigate]
        a1[(Merchant A wallet<br/>virtual account 9912345678)]
        a2[(Merchant B wallet<br/>virtual account 9912345679)]
    end
    p1([Buyer]) -->|bank transfer| a1
    p2([Buyer]) -->|bank transfer| a2
    a1 -.->|account / inflow| hook[Your webhook endpoint]
    a2 -.->|account / inflow| hook
    hook --> ledger[(Your merchant ledger)]
    a1 -->|settlement payout, onBehalfOf A| b1([Merchant A's bank])
    a2 -->|settlement payout, onBehalfOf B| b2([Merchant B's bank])
```

Each merchant gets a **wallet** (a balance in one currency on Multigate's ledger) with a **virtual account**: a bank account number in the merchant's name, issued by a partner bank, that credits the wallet. Buyers pay that number by ordinary bank transfer.

## Before you start

- [ ] A sandbox API key, and `MG_BASE_URL`, `MG_API_KEY` and `MG_CLIENT_ID` set as in the [Quickstart](../01-get-started/03-quickstart.md).
- [ ] A webhook URL registered **before** you open accounts ([Webhooks overview](../03-webhooks-and-emails/01-overview.md)), with [signature verification](../03-webhooks-and-emails/03-verifying-signatures.md).
- [ ] Your API user's permissions: *manage customers* (and *approve customers* to skip a second approver), *manage accounts*, *manage beneficiaries* and *initiate payment*.
- [ ] Multigate has set up an NGN partner bank for you, and you have accepted its terms of service in the dashboard.
- [ ] Ask Multigate whether **auto-sweep** to a pool account is set up for your business. With auto-sweep, every inflow is credited to one pool account of yours instead of the merchant's wallet, which changes how you settle and refund (see below).

## Build plan

<Steps>
<Step title="Onboard each merchant">

Create the merchant with [Create a customer](api:createSubClient), add its directors and owners, upload its documents and submit it: follow [Customers](../04-products/01-customers.md) step by step. Store the returned `entityId`: it's the **customer ID** you send as `onBehalfOf` from now on.

Wait for [`client` / `subclient_approved`](webhook:client.subclient_approved) and **check `status`**: `active` means approved, `rejected` means declined (the same event is used for both). In the sandbox, reviews can take days: ask your Multigate contact to complete them.

</Step>
<Step title="Open an account per merchant">

Once the merchant is `active`, call [Create an account](api:createAccount) with `onBehalfOf` set to the customer ID and a new `uniqueRequestId`:

```bash
curl -X POST "$MG_BASE_URL/client-service/api/v1/accounts" \
  -H "mp-api-key: $MG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "uniqueRequestId": "acme_acct_kola_ngn", "currencyIso3Code": "NGN", "accountName": "Kola Stores Collections", "onBehalfOf": "Kx81mQ2pZr4tV9wYa3Bc5" }'
```

Store `account.accountId` and every `fundingDetails[].accountNumber` against the merchant, and show the merchant their account number. Payers see the merchant's registered business name (`fundingDetails[].accountName`).

If `status` is `processing`, `fundingDetails` is empty: wait for [`account` / `account_opened`](webhook:account.account_opened) (its `clientId` is the customer ID). An account a bank refuses later sends no webhook, so poll [Get an account by ID](api:getAccountById) for accounts that stay `processing`. See [Accounts › When the account isn't active yet](../04-products/02-accounts.md#when-the-account-isnt-active-yet).

</Step>
<Step title="Credit orders from the inflow webhook">

Every payment sends [`account` / `inflow`](webhook:account.inflow) to **your** webhook URL. The payload has no customer ID, so:

1. Verify the signature, then store the `Idempotency-Key` header.
2. Skip it if you've already recorded its `transactionId` (the same inflow can be delivered twice).
3. Find the merchant by `receivingAccountNumber` (the number the buyer paid into), never by `accountId`.
4. Parse `amount` (a decimal string) as a decimal, and match the order by `narration` or your own order logic.
5. Store `transactionId` (`IN-…`): you need it for refunds and reconciliation.

Details: [Collections › The inflow webhook](../04-products/03-collections.md#the-inflow-webhook). In the sandbox, test with [Simulate an inflow](api:simulateInflow).

</Step>
<Step title="Save each merchant's settlement bank account">

Check the merchant's own bank account with [Validate a beneficiary account (NGN)](api:validateBeneficiaryAccount), then save it with [Create a beneficiary](api:createBeneficiary), passing the customer ID in `subClientId`. Store `beneficiary.entityId` as the merchant's settlement `beneficiaryId`. See [Beneficiaries for your customers](../04-products/04-beneficiaries.md#beneficiaries-for-your-customers).

</Step>
<Step title="Settle merchants">

Pay each merchant from their own wallet with [Create a transfer](api:createSingleTransfer), with `onBehalfOf` = the customer ID:

```bash
curl -X POST "$MG_BASE_URL/payment-service/api/v1/transfer/single-transfer" \
  -H "mp-api-key: $MG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "uniqueRequestId": "acme_settle_kola_20261010",
    "reference": "Settlement 10 Oct",
    "amount": 480000,
    "sourceCurrencyIso3Code": "NGN",
    "sourceAccountNumber": "9912345678",
    "onBehalfOf": "Kx81mQ2pZr4tV9wYa3Bc5",
    "beneficiaryId": "Bn4kQ8rT2vX6yZ1aC3dEf"
  }'
```

- The source account and the beneficiary must both belong to the merchant ("Invalid Source Account Owner" or "Invalid Destination Account Owner" otherwise).
- The fee is charged to the merchant's account, on top of the amount: leave room for it.
- Many merchants at once? Use [Bulk payouts](../04-products/06-bulk-payouts.md): one file per source account.

**With auto-sweep,** the merchant's wallet doesn't hold the money: settle from **your pool account** without `onBehalfOf`, to a beneficiary saved for your own business.

Track each payout as in [Payouts](../04-products/05-payouts.md#transfer-statuses).

</Step>
</Steps>

## Data to store

| Field | From | Used for |
|---|---|---|
| Customer ID (`entityId`) | [Create a customer](api:createSubClient) | `onBehalfOf` on accounts and payouts, `subClientId` on beneficiaries |
| `accountId` and `fundingDetails[].accountNumber` | [Create an account](api:createAccount) or `account_opened` | Routing inflows, settlement source, statements |
| Settlement `beneficiaryId` | [Create a beneficiary](api:createBeneficiary) | Settlement payouts |
| Inflow `transactionId` (`IN-…`) and `Idempotency-Key` | `account` / `inflow` | De-duplication, refunds, reconciliation |
| Payout `uniqueRequestId` (stored before the call) and `transactionId` | your own, then the transfer response | Matching transfer webhooks, lookups |

## Webhooks to handle

| Event | What to do |
|---|---|
| [`client` / `subclient_approved`](webhook:client.subclient_approved) | Enable the merchant if `status` is `active`; show the reason (`approvalComments`) if `rejected`. |
| [`account` / `account_opened`](webhook:account.account_opened) | Save the new `fundingDetails` (it can arrive once per partner bank). |
| [`account` / `inflow`](webhook:account.inflow) | Credit the merchant found by `receivingAccountNumber`. |
| [`transfer` / `transfer_completed`, `transfer_failed`](webhook:transfer.transfer_completed) | Update the settlement. `onBehalfOf` names the merchant. |
| [`transfer` / `refund`](webhook:transfer.refund) | Record the refund outcome, matched by your `uniqueRequestId`. |

Held or rejected inflows, approver declines and accounts that fail later send no webhook. See [Outcomes without a webhook](../03-webhooks-and-emails/02-event-catalogue.md#outcomes-without-a-webhook).

## Reconciliation

Each morning, for each merchant wallet (or your pool account with auto-sweep), pull yesterday's [Get an account statement](api:getAccountStatement) with a `pageSize` large enough for the whole day:

- Match credits to the inflows you recorded by `id` (= inflow `transactionId`). A credit you didn't record is a missed webhook: process it now.
- Match debits to settlements by `customReference` (= your `uniqueRequestId`), and account for `Fee` lines.
- Compare `closingBalance` with the balance in your merchant ledger.

[List accounts](api:listAccounts) with `onBehalfOf` lists one merchant's accounts. See [Balances and statements](../04-products/10-balances-and-statements.md#daily-reconciliation).

## Variations

- **One payment per order:** use [dynamic accounts](../04-products/02-accounts.md#dynamic-accounts). Each accepts one payment of an exact amount, then closes; payments land in your pool account, and a wrong amount sends no webhook.
- **Invoices with partial payments:** use [Receivables](../04-products/11-receivables.md).
- **Refunds to buyers:** [Refund an inflow](api:refundInflow) to the original sender (NGN and GHS, one refund per inflow). See [Refunds](../04-products/08-refunds.md).

## Things that trip people up

- All merchants' events come to your one webhook URL. Route inflows by `receivingAccountNumber`; with auto-sweep, `accountId` is the pool account's.
- "Payment Received" emails for every merchant go to **your** business contact, not to the merchant.
- Only `active` merchants can have accounts ("Sub-Client not active" otherwise), and a newly approved merchant can take a few minutes to become usable for payouts ("Invalid sub onBehalfOf").
- Give each merchant a distinct business name: two customers with the same name can't both be used for payouts.
- [List accounts](api:listAccounts) without `onBehalfOf` lists **your** accounts only. With the ID of a merchant that isn't `active`, it falls back to your own accounts.
- Refunds check the balance of the account that received the payment. With auto-sweep that account holds nothing, so refunds of swept inflows (including dynamic accounts) are refused; contact Multigate to return such a payment.

## Go-live notes

- Confirm with Multigate whether auto-sweep is on in production, and build settlement and refunds for the setting you'll have.
- Merchants' reviews in production depend on Multigate compliance; keep merchants "under review" in your product until `subclient_approved` arrives.
- Use real email domains for merchants: production refuses disposable ones.
- Work through the [go-live checklist](../01-get-started/07-go-live-checklist.md).
