# Invoice collections

Collect what your customers owe you, invoice by invoice, and stop matching payments to invoices by hand.

**Who it's for:** SMEs and corporate enterprises that issue invoices.

## Pick the right tool

| Situation | Use |
|---|---|
| Invoices that may be paid in parts, or overpaid | [Receivables](../04-products/11-receivables.md): one dedicated account per invoice, tracked as `partially_paid`, `paid` or `overpaid`. This page builds this option. |
| One exact payment per order (NGN) | A [dynamic account](../04-products/02-accounts.md#dynamic-accounts): accepts one payment of the exact amount, then closes. Payments land in your pool account. |
| Regular customers paying many invoices | A named virtual account per customer, matching payments by narration ([Collections](../04-products/03-collections.md)). |

A **receivable** is an invoice you register with Multigate. Each one gets its own virtual account (a bank account number at a partner bank), so every payment into it belongs to that invoice, whatever its narration or amount. Collected money is credited to your **receivable pool account** for that currency.

## Architecture

```mermaid
flowchart LR
    you[Your billing system] -->|create receivable| mg[Multigate API]
    mg --> r1[(Invoice INV-2026-0042<br/>its own account)]
    mg --> r2[(Invoice INV-2026-0043<br/>its own account)]
    payer1([Payer]) -->|bank transfer| r1
    payer2([Payer]) -->|bank transfer| r2
    r1 -->|credited| pool[(Your receivable pool account)]
    r2 -->|credited| pool
    mg -.->|receivables / inflow| you
```

```mermaid
sequenceDiagram
    autonumber
    participant You as Your backend
    participant MG as Multigate
    participant Customer as Your customer
    You->>MG: Create a receivable (invoiceRef, amount, dueDate, uniqueRequestId)
    MG-->>You: entityId, status processing, fundingDetails empty
    loop Until status is unpaid or failed
        You->>MG: Get a receivable
    end
    You->>Customer: Invoice with the account from fundingDetails
    Customer->>MG: Pays, in one or several transfers
    MG-)You: receivables / inflow (status, amountPaid)
    MG-)You: account / inflow
    Note over You: Close the invoice when status is paid or overpaid
```

## Before you start

- [ ] A sandbox API key, and `MG_BASE_URL` and `MG_API_KEY` set as in the [Quickstart](../01-get-started/03-quickstart.md).
- [ ] Multigate has set up, **per currency**, a receivable pool account and a receiving bank that supports receivables. Without them, creating a receivable fails with `400`.
- [ ] Your API user has the *create receivables* and *view receivables* permissions.
- [ ] A webhook URL registered ([Webhooks overview](../03-webhooks-and-emails/01-overview.md)).
- [ ] Receivables are for your own business only: there's no `onBehalfOf`.

## Build plan

<Steps>
<Step title="Create a receivable per invoice">

Generate a **UUID** as `uniqueRequestId` (it must be unique across Multigate), store it with the invoice, then call [Create a receivable](api:createReceivable):

```bash
curl -X POST "$MG_BASE_URL/client-service/api/v1/receivables" \
  -H "mp-api-key: $MG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "invoiceRef": "INV-2026-0042",
    "amount": 500000,
    "currencyIso3Code": "NGN",
    "payer": "Kola Stores Ltd",
    "issueDate": "2026-10-01",
    "dueDate": "2026-10-31",
    "uniqueRequestId": "4f6a0e1c-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
  }'
```

Store `entityId` (the receivable ID). The response is `status` `processing` with an empty `fundingDetails`: the account is opened in the background.

If the call times out, look for your `uniqueRequestId` with [List receivables](api:listReceivables) before retrying. A reused ID is refused with `400` and the original isn't returned.

</Step>
<Step title="Wait for the invoice's account">

No webhook tells you the account is ready. Poll [Get a receivable](api:getReceivable) (for example every 10 seconds for a few minutes) until `status` is:

- `unpaid`: ready. Put the account from `fundingDetails` (account name, number, bank) on the invoice.
- `failed`: the account couldn't be opened. Create a new receivable with a new UUID.

If the bank opens the account after a delay, you also get [`account` / `account_opened`](webhook:account.account_opened) with `accountType` `receivable` and the receivable's `uniqueRequestId`: treat it as a signal to poll. Never send the invoice while `status` is still `processing`.

</Step>
<Step title="Process each payment">

Every payment sends [`receivables` / `inflow`](webhook:receivables.inflow), and also [`account` / `inflow`](webhook:account.inflow). Handle the receivables event:

1. Verify the signature and store the `Idempotency-Key`.
2. Skip it if you've already recorded `transactionDetails.transactionId`.
3. Find the invoice by `receivableDetails.entityId` or `receivableDetails.uniqueRequestId`.
4. Update it from `receivableDetails.status` and `receivableDetails.amountPaid` (the running total).

| `status` | Meaning | Your action |
|---|---|---|
| `partially_paid` | `amountPaid` is less than `amount`. | Show the balance due. |
| `paid` | Exactly the invoice amount received. The account is closed. | Close the invoice. |
| `overpaid` | More than the invoice amount received. The account is closed. | Close the invoice and handle the excess (below). |

[Get a receivable](api:getReceivable) lists every payment in `payments`, if you miss an event.

</Step>
<Step title="Handle overpayments and late payments">

A payment that arrives after `paid`, before the bank closes the account, is added to the total and makes the receivable `overpaid`, but that money stays in the receivable's own account instead of your pool account. Contact Multigate to have it moved.

To return an excess, contact Multigate. A [refund](../04-products/08-refunds.md) checks the balance of the account the payment was made into, which for a receivable is the receivable's own account, not your pool account, so it's refused today ("Insufficient balance in source account" or "Could Not Retrieve Account Balance").

</Step>
<Step title="Report">

- [List receivables](api:listReceivables): newest first, filtered by `status` and `currencies`. It returns `count` (items on this page) and `totalCount`, not the usual paging fields.
- [Get a receivables summary](api:getReceivablesSummary): totals invoiced and collected per currency.
- [Get receivables history](api:getReceivablesHistory): the same totals per month, for the current month and the 5 before it.

</Step>
</Steps>

## Data to store

| Field | From | Used for |
|---|---|---|
| `uniqueRequestId` (a UUID, stored before the call) | your own | Lookups after a timeout, matching `account_opened` |
| Receivable `entityId` | [Create a receivable](api:createReceivable) | Polling, matching webhooks |
| `fundingDetails` | [Get a receivable](api:getReceivable) once `unpaid` | The account on the invoice |
| `status` and `amountPaid` | `receivables` / `inflow` | Invoice state |
| Each payment's `transactionDetails.transactionId` (`IN-…`) | `receivables` / `inflow` | De-duplication, reconciliation |

## Webhooks to handle

| Event | What to do |
|---|---|
| [`receivables` / `inflow`](webhook:receivables.inflow) | Update the invoice from `status` and `amountPaid`. |
| [`account` / `inflow`](webhook:account.inflow) | Sent for the same payment. Ignore it for receivables, or use it only as a cross-check by `transactionId`. |
| [`account` / `account_opened`](webhook:account.account_opened) (`accountType` `receivable`) | Poll the receivable: its account may now be ready. |

The receivable becoming `unpaid` or `failed` sends no webhook. Each payment also sends a "Payment Received" email to your business contact.

## Reconciliation

Each day, pull yesterday's [Get an account statement](api:getAccountStatement) for your **receivable pool account**. Match each credit's `id` to a `transactionDetails.transactionId` you recorded, and account for collection fees. A credit you didn't record is a missed webhook: fetch the receivables with [List receivables](api:listReceivables) and process it. See [Balances and statements](../04-products/10-balances-and-statements.md#daily-reconciliation).

## Things that trip people up

- There's no webhook when a receivable's account is ready: poll after creating it.
- `uniqueRequestId` must be a UUID ("uniqueRequestId must be a valid GUID" otherwise).
- For USD, the account is limited to the invoice amount and expires 60 days after `dueDate`; depending on the bank, a larger or later payment is refused.
- A dynamic account rejects a payment of the wrong amount without any webhook. Tell payers to send the exact amount.

## Go-live notes

- Ask Multigate to set up the production receivable pool account and receiving bank for each currency you invoice in.
- Agree how overpayments and late payments are returned, since the API can't refund them today.
- Work through the [go-live checklist](../01-get-started/07-go-live-checklist.md).
