# Refunds

Send money from an inflow back to the person who paid it, for example a customer who overpaid or paid by mistake. An *inflow* is a payment someone made into one of your accounts; Multigate told you about it with the `account` / `inflow` webhook.

## Before you start

- [ ] `MG_BASE_URL` and `MG_API_KEY` are set, as in the [Quickstart](../01-get-started/03-quickstart.md).
- [ ] Your API user has the *initiate payment* or *approve payment* permission.
- [ ] You stored the inflow webhook ([Collections](03-collections.md)). From it you need:
  - `transactionId` (`IN-…`): which inflow to refund;
  - `senderAccountNumber`: where the money goes back to;
  - `senderAccountName`: the name on that account.
- [ ] You know the **sender's bank code**. The inflow webhook doesn't include it (its `bankCode` is **your** receiving bank), so ask the payer for their bank, and take its `bankCode` from [List banks](api:listBanks).
- [ ] The account that received the inflow still holds at least the refund amount.

## Rules

- **Which inflows:** your own `completed` inflows in **NGN or GHS**, identified by the inflow webhook's `transactionId` (`IN-…`).
- **How much:** all of it, or part with `amount`. **One refund per inflow:** while one is processing or after one completed, another is refused (`409`). After a partial refund, the rest can't be refunded through the API, so refund the full amount you intend in one go. A `failed` refund doesn't count; you can try again.
- **To whom:** the original sender. When the inflow recorded the sender's account number, `beneficiary.accountNumber` must be that number ("Beneficiary mismatch" otherwise). The sender's bank must be one Multigate can pay refunds to in that currency.
- **From where:** the account that received the inflow. Its available balance must cover the refund when you call, and again when the refund is paid, when the amount is reserved.
- **Known issue:** inflows credited to a pool account (auto-sweep, dynamic accounts, receivables) can't be refunded today: the check reads the receiving account, which holds nothing. Contact Multigate to return such a payment.
- **Fees:** no refund fee. The original collection fee isn't returned.
- **Approval and screening:** refunds don't wait for an approver, but they are screened like payouts.
- **Permission:** *initiate payment* or *approve payment*.

## How it works

```mermaid
sequenceDiagram
    autonumber
    participant You as Your backend
    participant MG as Multigate
    participant Bank as Sender's bank
    Note over You: You received account / inflow with transactionId IN-…
    You->>MG: POST /transaction/refund (transactionId, uniqueRequestId, beneficiary)
    MG->>MG: Check eligibility, balance and sender
    MG-->>You: 200, status processing
    MG->>MG: Screening, then reserve the amount
    MG->>Bank: Pay the sender
    Bank-->>MG: Result
    MG->>MG: Debit your account (on success)
    MG-)You: transfer / refund (completed or failed)
```

## Refund an inflow

Generate a new `uniqueRequestId` for the refund and store it, then send the request. Use the inflow webhook's `transactionId` **exactly as received**, and the sender's account from the same webhook.

```bash
curl -X POST "$MG_BASE_URL/payment-service/api/v1/transaction/refund" \
  -H "mp-api-key: $MG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionId": "IN-261010-1204557781",
    "uniqueRequestId": "rf_20261010_0001",
    "amount": 5000,
    "beneficiary": { "accountName": "KOLA STORES LTD", "accountNumber": "2093847561", "bankBicOrCode": "011" }
  }'
```

```json
{
  "message": "Successful",
  "transactionId": "IN-261010-1204557781",
  "uniqueRequestId": "rf_20261010_0001",
  "currency": "NGN",
  "amount": 5000,
  "status": "processing"
}
```

- `amount` must be a JSON **number** with up to two decimals, in major units (`5000.50`). Leave it out for a full refund.
- The response always says `processing`, and its `transactionId` is the **inflow's** ID. Track the refund by your `uniqueRequestId`.
- The outcome arrives as the `transfer` / `refund` webhook, with the inflow's `transactionId`, your `uniqueRequestId`, `amount` and `status` (`completed` or `failed`).
- If you get no webhook within an hour, look the refund up by your `uniqueRequestId` (see the caution below).
- A timeout or `5xx` means the outcome is unknown. Look the refund up by `uniqueRequestId` before you retry; a new request for the same inflow is refused with `409` while the first one is processing or completed, so a retry can't refund twice.

| Field | Where it comes from |
|---|---|
| `transactionId` | `transactionId` of the `account` / `inflow` webhook (`IN-…`). |
| `uniqueRequestId` | You generate it. Unique across Multigate. |
| `amount` | Optional. Leave it out to refund the whole inflow. |
| `beneficiary.accountNumber` | `senderAccountNumber` of the inflow webhook. |
| `beneficiary.accountName` | `senderAccountName` of the inflow webhook. |
| `beneficiary.bankBicOrCode` | The sender's bank code, from the payer and [List banks](api:listBanks). Not in the webhook. |

:::warning[Use the exact inflow transactionId]
Send the inflow's `transactionId` (`IN-…`) character for character. Don't send the bank's `paymentReference` or a shortened ID.
:::

## Refund statuses

| Status | Final | Webhook | Meaning |
|---|---|---|---|
| `pending` | No | — | Held for screening. |
| `processing` | No | — | Being paid. Also while Multigate reconciles a refund whose outcome at the bank is unclear, for example one the bank accepted and later reported failed; another refund of the same inflow gets `409` "Refund currently processing" meanwhile. |
| `completed` | Yes | `refund` | The sender was paid and your account debited. |
| `failed` | Yes | `refund` (not always) | Not paid; your account isn't debited. |

```mermaid
stateDiagram-v2
    direction LR
    [*] --> pending: screening
    [*] --> processing
    pending --> processing
    processing --> completed
    processing --> failed
    pending --> failed
```

:::caution[A failed refund can arrive without a webhook]
If the balance can't be reserved when the refund is about to be paid (for example you spent the money after calling), the refund ends `failed` and **no** `refund` webhook is sent. If you don't get a webhook, look the refund up with [Find transfers](api:listTransfers) `?uniqueRequestId=`.
:::

## Errors

A `4xx` means nothing was created.

| Message | Status | Cause | What to do |
|---|---|---|---|
| Transaction not found | `404` | Unknown `transactionId`, not yours, or not an inflow. | Use the inflow webhook's `transactionId` exactly. |
| Refunds are not supported for this transaction currency | `400` | Only NGN and GHS. | Refund outside the API. |
| Only a completed collection can be refunded | `400` | The inflow isn't completed. | Wait until it is. |
| Refund currently processing / Refund completed already | `409` | This inflow already has a refund. | Wait for its `refund` webhook. |
| The requested refund amount cannot be greater than the original transaction amount | `400` | `amount` is too high. | Send less. |
| Invalid amount | `400` | `amount` is zero or negative. | Send a positive amount. |
| Insufficient balance in source account | `400` | The receiving account can't cover it. | Fund it and retry. |
| Could Not Retrieve Account Balance | `400` | The balance couldn't be read. | Retry. |
| Beneficiary mismatch | `400` | The account isn't the original sender's. | Use the sender's account from the inflow webhook. |
| Refunds are not currently configured for this beneficiary's bank | `400` | Multigate can't pay refunds to the sender's bank in this currency. | Contact Multigate. |
| The original transaction is missing the sender's bank details required to process a refund | `400` | The inflow record is incomplete. | Contact Multigate support. |
| Unique request ID already exist | `400` | Reused `uniqueRequestId`. | Use a new ID; after a failed refund, retry with a new one too. |
| Validation error (`amount must be a number`) | `400` | `amount` sent as a string, or with more than two decimals. | Send a JSON number. |

## Next

- [Collections](03-collections.md) for the inflow webhook and its `transactionId`.
- [Event catalogue](../03-webhooks-and-emails/02-event-catalogue.md) for the `refund` webhook.
