Products
Payouts
Send money from one of your Multigate accounts to a bank account, in Nigeria or abroad, with one endpoint: Create a transfer. This page shows which request to send for your case, what happens after you send it, and how to handle every outcome.
Words used on this page
| Term | Meaning |
|---|---|
| Payout or transfer | Money sent from your account to someone else's bank account. The API calls it a transfer. |
| Beneficiary | The person or business you pay. |
| Source account | Your Multigate account that is debited (the money comes out of it). |
| Bank code | A bank's local identifier, for example 058 in Nigeria. Get it from List banks. |
| BIC (SWIFT code) | A bank's international identifier, 8 or 11 characters, for example COBADEFFXXX. |
| NUBAN | The 10-digit format of every Nigerian bank account number. |
| Rail | The network the payment travels on: NIP (Nigeria's instant transfers), RTGS (high-value local transfers) or SWIFT (international wires). Multigate picks it for you. |
unique |
An ID you generate for each payout. Multigate refuses a second request with the same ID, so a retry can never pay twice (this is called an idempotency key). |
| Screening | Multigate's automatic compliance check of each payment, before any money moves. |
| Approval (maker-checker) | A rule in your business that a second person must approve a payment someone else created. |
| Webhook | An HTTPS POST Multigate sends to your server when the payout finishes. |
More terms are in the Glossary.
When to use it
| You want to | Use |
|---|---|
| Pay one beneficiary now | Create a transfer, this page |
| Pay many beneficiaries from a file | Bulk payouts |
| Pay out for a remittance (IMTO) with sender details | Remittance |
| Send money back to someone who paid you | Refunds |
| Pay in a different currency from the one you hold | Convert first, then pay out from the target account |
Before you start
- You have done the Quickstart, so
MG_BASE_URLandMG_API_KEYare set in your terminal. If a call returns401, check thatMG_API_KEYholds the whole key and that it hasn't been revoked (When a request returns 401). - Your API user has the initiate payment or approve payment permission (
can_orinitiate_ payment can_inapprove_ payment permissionsof the current user). Without either, create calls return403. Getting a fee quote needs initiate payment. - You have an active source account in the currency you will send. Find it with List accounts: use one of its account numbers (
funding) asDetails[]. account Number source, or itsAccount Number accountIdassource. The Quickstart saved an NGN one inAccount Id MG_.ACCOUNT_ NUMBER - The account's available balance covers the amount plus the fee (see Fees). Check it with Get an account balance. In the sandbox, add money with Simulate an inflow.
- Your webhook URL is registered, and your endpoint verifies signatures. The result of a payout arrives there.
- Paying for a customer? You have the customer ID (
entityIdfrom Create a customer), and the source account belongs to that customer (List accounts withonBehalfOf). See Acting for customers. - USD to someone else? You need supporting documents (for example an invoice) when the beneficiary's account name doesn't match your source account's name. Ask Multigate how to upload them; you pass their IDs in
assets.
How a payout works
The create call only checks and records the transfer, then answers within seconds. Everything else happens after the response:
- Checks (during the call). Multigate checks the source account, the beneficiary, the currency, the amount limits and the fee (and your balance, for a large NGN transfer that is split, or a linked-account source). If any check fails, you get
400and nothing is created. - Approval (if your business uses it). The transfer waits for an approver in your business. See Approvals.
- Screening. Multigate checks the payment against its compliance and risk rules. Most payments clear in seconds; some are held for manual review.
- Balance check and debit. Multigate checks that your available balance covers the amount plus the fee, then debits it. If the balance is too low, the transfer ends
failed(responseCode22) with atransfer_webhook, and nothing is debited. Your balance isn't reserved before this point.failed - Payment. The money is sent to the beneficiary's bank on the right rail.
- Result. You receive a
transfer_orcompleted transfer_webhook.failed
sequenceDiagram
autonumber
participant You as Your backend
participant MG as Multigate
participant Bank as Beneficiary bank
You->>MG: POST /transfer/name-enquiry (optional, local)
MG-->>You: Account name
You->>MG: GET /transfer/get-transfer-fee (optional)
MG-->>You: Fee
You->>MG: POST /transfer/single-transfer (uniqueRequestId)
Note over MG: Check account, beneficiary, limits and fee
MG-->>You: 200 OK, status pending, pending_approval or processing
Note over MG: Your approval rules, then compliance screening
Note over MG: Check balance, then debit amount and fee
MG->>Bank: Send payment on the chosen rail
Bank-->>MG: Result
MG-)You: Webhook transfer_completed or transfer_failed
Choose your request
Every request has the same core fields. What changes is how you describe the beneficiary.
| Your case | source |
Beneficiary | beneficiary |
Extra fields | Example in Send a payout |
|---|---|---|---|---|---|
| Pay a Nigerian bank account in naira | NGN |
Full beneficiary |
Local bank code from List banks (country) |
accountNumber must be the 10-digit NUBAN |
NGN local tab |
| Pay a local account in GHS, XOF or ETB | That currency | Full beneficiary |
Local bank code for the beneficiary's country | country must be the bank's country |
Other local currency tab |
| Pay abroad in USD, EUR, GBP… | That currency | Full beneficiary |
The bank's BIC | postCode; chargeType for SWIFT; intermediary if the bank needs one; assets for USD to someone else |
International tab |
| Pay someone you've paid or saved before | Any | beneficiaryId only |
Not sent | None | Saved beneficiary tab |
| Pay from a customer's account | Any | Either of the above | As above | onBehalfOf = customer ID |
For a customer tab |
The beneficiary's currency must be the source account's currency. To pay in another currency, convert first.
Request fields
| Field | Required | Where the value comes from |
|---|---|---|
unique |
Yes | You generate it: one per payout, unique across all of Multigate. Use a UUID with your own prefix, for example acme_po_6f1c…. Store it before you call. |
reference |
Yes | Your description of the payment, 5 to 100 characters. The beneficiary sees the first 36 characters as the narration. |
amount |
Yes | Amount the beneficiary receives, in major units of the source currency (250000 is 250,000.00 NGN; 1200.50 is 1,200.50 USD). At most 2 decimals. |
source |
Yes | The source account's currency, upper case (NGN). |
source |
One of these two | An account number of your source account, from List accounts. |
source |
The source account's accountId, from List accounts. |
|
beneficiary |
One of these two | The full bank details. Multigate saves them as a beneficiary (or reuses the matching saved one). |
beneficiaryId |
A saved beneficiary's ID: beneficiary. from Create a beneficiary, or beneficiary from an earlier transfer. |
|
onBehalfOf |
Only for a customer | The customer ID. Leave it out to pay from your own business's account. |
chargeType |
SWIFT only | Who pays the banks' SWIFT charges: OUR (you), SHA (shared, the default) or BEN (the beneficiary). |
paymentMethod |
No | Leave it out: Multigate picks the rail. See Which rail is used. |
assets |
USD to someone else | IDs of uploaded supporting documents. |
client |
No | Your own reference, stored with the transfer. |
beneficiary needs name, accountName, accountNumber, bankBicOrCode, currency, country, addressLine1, city and state. The full list, with lengths, is in Create a transfer.
Send a payout
For a local account you haven't paid before, ask the bank for the name on the account with Look up an account name, and compare it with the name your user expects. This catches typing mistakes before money moves.
Find the bank code first:
curl -s "$MG_BASE_URL/payment-service/api/v1/banks?countryIso3Code=NGA&bankName=guaranty" \
-H "mp-api-key: $MG_API_KEY" \
| jq '.data[] | {bankName, bankCode, swift_code, isActive}'
Then look up the account:
curl -s -X POST "$MG_BASE_URL/payment-service/api/v1/transfer/name-enquiry" \
-H "mp-api-key: $MG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "bankCode": "058", "bankAccountNumber": "0123456789", "currencyIso3Code": "NGN", "countryIso3Code": "NGA" }'
{
"message": "Successful",
"responseCode": "00",
"responseMessage": "Successful",
"status_code": "00",
"status_desc": "successful",
"bankAccountName": "ADA OKAFOR",
"bankAccountNumber": "0123456789",
"bankName": "Guaranty Trust Bank",
"accountCurrency": "NGN"
}
The lookup answers with HTTP 200 even when the account isn't found. Only responseCode 00 means found; anything else means don't pay that account yet.
curl -s "$MG_BASE_URL/payment-service/api/v1/transfer/get-transfer-fee?amount=250000&sourceCurrencyIso3Code=NGN&sourceAccountNumber=$MG_ACCOUNT_NUMBER" \
-H "mp-api-key: $MG_API_KEY"
{
"message": "Successful",
"feeAmount": 50,
"feeCurrency": "NGN",
"isFeePercentage": false,
"transactionCharge": 50,
"beneficiaryWillReceive": 250000
}
Show your user amount + feeAmount: that is what leaves the account. For NGN amounts above 100,000,000, use Validate a transfer instead: its split is the exact fee for a split transfer.
Generate a new ID for this payout and save it in your database before you call Multigate, with your payout's status set to "sending". If the call times out, this ID is how you find out what happened.
export MG_REQUEST_ID="acme_po_$(uuidgen | tr 'A-Z' 'a-z')"
Replace acme with a short prefix for your business: the ID must be unique across every Multigate client, not only yours.
Pick the tab for your case. Each request is complete: every required field is there.
Pay a Nigerian bank account in naira from your NGN account.
curl -s -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 @- <<EOF
{
"uniqueRequestId": "$MG_REQUEST_ID",
"reference": "Invoice 2026-118",
"amount": 250000,
"sourceCurrencyIso3Code": "NGN",
"sourceAccountNumber": "$MG_ACCOUNT_NUMBER",
"beneficiaryDetails": {
"name": "Ada Okafor",
"accountName": "ADA OKAFOR",
"accountNumber": "0123456789",
"bankBicOrCode": "058",
"currencyIso3Code": "NGN",
"countryIso3Code": "NGA",
"addressLine1": "12 Marina Road",
"city": "Lagos",
"state": "Lagos"
}
}
EOF
accountNumbermust be exactly 10 digits. See the warning in Errors.- Use the name from the name enquiry as
accountName. - NGN amounts above 100,000,000 are split into several payments.
Pay a local account in GHS, XOF or ETB from an account in the same currency. The shape is the same as NGN; the bank code comes from List banks for the beneficiary's country (here country). Here MG_ holds an account number of your GHS account.
curl -s -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 @- <<EOF
{
"uniqueRequestId": "$MG_REQUEST_ID",
"reference": "Supplier payment Oct",
"amount": 4800,
"sourceCurrencyIso3Code": "GHS",
"sourceAccountNumber": "$MG_GHS_ACCOUNT_NUMBER",
"beneficiaryDetails": {
"name": "Kofi Mensah",
"accountName": "Kofi Mensah",
"accountNumber": "1441000123456",
"bankBicOrCode": "130100",
"currencyIso3Code": "GHS",
"countryIso3Code": "GHA",
"addressLine1": "5 Independence Avenue",
"city": "Accra",
"state": "Greater Accra"
}
}
EOF
countrymust be the bank's country: local bank codes repeat across countries.Iso3 Code - Multigate doesn't check these account numbers with the bank before paying, so check them with your user.
Pay a bank abroad. Here a German supplier is paid in USD from your USD account (MG_).
curl -s -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 @- <<EOF
{
"uniqueRequestId": "$MG_REQUEST_ID",
"reference": "PO 7781 Lumen Trading",
"amount": 18000,
"sourceCurrencyIso3Code": "USD",
"sourceAccountNumber": "$MG_USD_ACCOUNT_NUMBER",
"chargeType": "SHA",
"assets": ["3c9d7e21-5b4a-4f6e-8a1d-2e7f9c0b6a51"],
"beneficiaryDetails": {
"name": "Lumen Trading GmbH",
"accountName": "Lumen Trading GmbH",
"accountNumber": "DE89370400440532013000",
"bankBicOrCode": "COBADEFFXXX",
"currencyIso3Code": "USD",
"countryIso3Code": "DEU",
"addressLine1": "Friedrichstrasse 10",
"city": "Berlin",
"state": "Berlin",
"postCode": "10117",
"intermediaryBank": {
"bankName": "Citibank N.A.",
"bic": "CITIUS33XXX",
"countryIso3Code": "USA"
}
}
}
EOF
bankBicOrCodeis the BIC. It must be in Multigate's bank list; an unlisted 11-character BIC is matched on its first 8 characters.accountNumberis the account number or IBAN, letters and digits only.intermediaryis the correspondent bank that some beneficiary banks need for USD. Leave it out if the beneficiary didn't give you one. In requests its fields areBank bankName,bicandcountry; transfer webhooks return it asIso3 Code bankName,bankCodeandswiftCode.assetsis required for USD only whenbeneficiarydoesn't match your source account's name (a third-party payment). Leave it out when paying your own account abroad.Details. account Name - EUR, GBP and other foreign currencies use the same shape. Some go through a local rail abroad (for example SEPA for EUR) instead of SWIFT: see Which rail is used.
- USD payouts are completed by Multigate's operations team: they stay
processinguntil then.
Pay a beneficiary you saved earlier. Put its ID in MG_: beneficiary. from Create a beneficiary, or beneficiary from an earlier transfer's response.
curl -s -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 @- <<EOF
{
"uniqueRequestId": "$MG_REQUEST_ID",
"reference": "Invoice 2026-119",
"amount": 75000,
"sourceCurrencyIso3Code": "NGN",
"sourceAccountNumber": "$MG_ACCOUNT_NUMBER",
"beneficiaryId": "$MG_BENEFICIARY_ID"
}
EOF
- Send
beneficiaryIdorbeneficiary, never both: both together fail validation.Details - The beneficiary must be in the source account's currency, and belong to the same business or customer as the source account.
Pay from one of your customer's accounts. MG_ is the customer's entityId from Create a customer; MG_ is an account number from List accounts called with on.
curl -s -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 @- <<EOF
{
"uniqueRequestId": "$MG_REQUEST_ID",
"reference": "Kola Stores payroll Oct",
"amount": 120000,
"sourceCurrencyIso3Code": "NGN",
"sourceAccountNumber": "$MG_CUSTOMER_ACCOUNT_NUMBER",
"onBehalfOf": "$MG_CUSTOMER_ID",
"beneficiaryDetails": {
"name": "Tunde Bello",
"accountName": "TUNDE BELLO",
"accountNumber": "0234567891",
"bankBicOrCode": "058",
"currencyIso3Code": "NGN",
"countryIso3Code": "NGA",
"addressLine1": "3 Allen Avenue",
"city": "Ikeja",
"state": "Lagos"
}
}
EOF
- The source account must belong to that customer ("Invalid Source Account Owner" otherwise).
- The beneficiary is saved for the customer, not for your business. A
beneficiaryIdmust also be the customer's (saved withsubClientId). - The customer must be
active. A newly approved customer can take a few minutes to become usable here ("Invalid sub onBehalfOf" until then). - Webhooks still come to your URL;
onBehalfOfin the payload tells you which customer.
A successful call returns 200 with the transfer:
{
"message": "Successful",
"transactionId": "PMT-261010-093000482913",
"uniqueRequestId": "acme_po_6f1c2a90-3b7d-4e8a-9c51-0d2e4f6a8b13",
"reference": "Invoice 2026-118",
"status": "pending",
"responseCode": null,
"responseMessage": "",
"amount": "250000.00",
"charge": "50.00",
"totalAmount": "250050.00",
"sourceAccountNumber": "1029384756",
"sourceCurrencyIso3Code": "NGN",
"beneficiaryId": null,
"beneficiaryDetails": {
"entityId": "Bn4kQ8rT2vX6yZ1aC3dEf",
"accountName": "ADA OKAFOR",
"accountNumber": "0123456789",
"bankName": "Guaranty Trust Bank",
"bankBicOrCode": "058",
"currencyIso3Code": "NGN",
"countryIso3Code": "NGA"
},
"transactionType": "DR",
"createdAt": "2026-10-10T09:30:00.482Z"
}
The response is shortened here. Store these fields with your payout:
| Field | Why |
|---|---|
transactionId |
Multigate's ID for the transfer (PMT-…). Support and receipts use it. |
status, responseCode |
Where the transfer is now. pending with responseCode null means it is being screened. |
charge, totalAmount |
The fee, and what will be debited (amount + charge). Amounts are decimal strings: parse them as decimals, never as floating-point numbers. |
beneficiary |
The saved beneficiary. Send it as beneficiaryId next time. |
source in the response can differ from the number you sent: it shows the account Multigate debits. beneficiaryId is null when you sent beneficiary.
After you send
The create response is never the final result. Follow these steps for every payout.
Save transactionId and status from the create response. The status is one of:
status |
responseCode |
Meaning | What you do |
|---|---|---|---|
pending |
null |
Being screened. | Wait for the webhook. |
pending_ |
42 |
Waiting for an approver in your business. | Ask your approver to act in the dashboard, then wait for the webhook. |
processing |
01 |
Sent for payment. | Wait for the webhook. |
Show your user "processing", never "paid", at this point.
When the payout finishes, Multigate sends transfer / transfer_ or transfer / transfer_ to your webhook URL. Verify the signature first (Verifying signatures), answer 200 quickly, then process the event:
// Called after the signature check, with the parsed webhook body.
async function handleTransferEvent(event) {
const { notification_type: type, notification } = event.header;
if (type !== 'transfer') return;
if (notification !== 'transfer_completed' && notification !== 'transfer_failed') return;
const { uniqueRequestId, transactionId, responseCode, responseMessage } = event.body;
// A split NGN transfer sends one event per part: acme_po_…-1, acme_po_…-2 (see "Large NGN transfers").
const payout = await db.payouts.findByUniqueRequestId(uniqueRequestId);
if (!payout) return; // not one of yours
// Webhooks can arrive twice or out of order: never change a payout that is already final.
if (['completed', 'failed', 'rejected'].includes(payout.status)) return;
if (notification === 'transfer_completed') {
await db.payouts.update(payout.id, { status: 'completed', transactionId });
} else {
await db.payouts.update(payout.id, { status: 'failed', transactionId, responseCode, failureReason: responseMessage });
}
}
transfer_means the beneficiary has been paid. Mark it paid.completed transfer_means it wasn't paid; for a bank or partner failure the debit is reversed to your account. Showfailed responseto your user. To try again, create a new transfer with a newMessage unique.Request Id - Payload fields are in the Event catalogue.
Some final outcomes send no webhook: rejected, a compliance decline, and a debit that couldn't be posted (see Transfer statuses). Webhook delivery also stops after 5 failed retries. So run a job that checks every payout still non-final in your system 30 minutes after it was created, then every hour:
curl -s "$MG_BASE_URL/payment-service/api/v1/transaction/report?uniqueRequestId=$MG_REQUEST_ID,$MG_REQUEST_ID-1" \
-H "mp-api-key: $MG_API_KEY" \
| jq '.data[] | {uniqueRequestId, transactionId, status, responseCode, responseMessage}'
{
"uniqueRequestId": "acme_po_6f1c2a90-3b7d-4e8a-9c51-0d2e4f6a8b13",
"transactionId": "PMT-261010-093000482913",
"status": "completed",
"responseCode": "00",
"responseMessage": "Successful"
}
Apply a final status (completed, failed, rejected) exactly as you would from a webhook. The -1 also finds a split NGN transfer. In the sandbox, results can take 15 minutes or more; don't poll more often than every few minutes.
If a payout is still not final after 24 hours, or stays pending with responseCode null for more than a few minutes, contact support with its transactionId.
A timeout, a 5xx or a dropped connection on create means the outcome is unknown: the transfer may or may not have been created. The create call waits only for the checks (not for the bank), and the production gateway gives up after 60 seconds.
- Don't send a new transfer with a new
unique: that can pay twice.Request Id - Look the payout up with the poll command above (
?unique).Request Id=<yours>,<yours>-1 - Found: the transfer exists. Save its
transactionIdand continue from step 1. - Not found: wait a minute and look again. Still not found: send the same request again with the same
unique. If the first attempt was created in the meantime, you getRequest Id 400"Unique request ID already exist" and nothing is paid twice; look it up again. - If the same
uniqueis sent twice at the same moment (for example by two of your servers), only one transfer is created and the other request getsRequest Id 400"Unique request ID already exist". Still let one worker own each payment, so you know which answer to keep.
See Idempotency for the full pattern.
For a completed transfer, Download a transfer receipt returns a PDF:
curl -s -o receipt.pdf "$MG_BASE_URL/payment-service/api/v1/transaction/download-transfer-receipt/PMT-261010-093000482913" \
-H "mp-api-key: $MG_API_KEY"
Transfer statuses
stateDiagram-v2
direction LR
[*] --> pending: created, screening
[*] --> pending_approval: needs an approver
[*] --> processing: not screened (linked account)
pending --> processing: screening cleared
pending --> failed: declined by compliance (43)
pending_approval --> processing: approved (05), then screened
pending_approval --> rejected: declined by approver
processing --> pending: bank unreachable, retrying (31)
processing --> completed: paid
processing --> failed: bank, partner or debit failure
processing --> rejected: declined in Multigate review
completed --> [*]
failed --> [*]
rejected --> [*]
| Status | Final? | responseCode |
Meaning | Webhook | What to do |
|---|---|---|---|---|---|
pending |
No | null |
Being screened. | None | Wait. |
pending |
No | 31 |
The bank or partner couldn't be reached. Multigate retries a few times. | None | Wait. Contact support if it stays pending for hours. |
pending |
No | other | A manually settled payout (for example USD) is being completed by Multigate's operations team. | None | Wait for the webhook. |
pending_ |
No | 42 |
Waiting for an approver in your business. | None | Ask your approver to act in the dashboard. |
processing |
No | 01 (or 05 just after approval) |
Sent for payment. USD payouts, and some other currencies, are completed by Multigate's operations team and stay processing until then. With some banks, a reported failure also leaves the payout processing while Multigate's operations team confirms it: those banks sometimes report a payment as failed when it went through, or before the money is back, so Multigate marks it failed (and returns the money to your account) only once that is checked. Where the bank's failure is definitive, the payout fails straight away. |
None | Wait for the webhook, and poll payouts that stay processing unusually long; contact support with the transactionId if one doesn't move. |
completed |
Yes | 00 |
The beneficiary has been paid. | transfer_ |
Mark it paid. |
failed |
Yes | Varies | Not paid. For a bank or partner failure the debit is reversed to your account. | transfer_, with the exceptions below |
Read response. Fix and send a new transfer if appropriate. |
failed |
Yes | 43 |
Declined by Multigate compliance ("Payment ongoing review") before any debit. | None | Contact support. |
failed |
Yes | 22 |
Your balance (or separate fee account) didn't cover amount plus fee when Multigate checked it, just before the debit. Nothing was debited. | transfer_ |
Fund the account, then send a new transfer with a new unique. |
failed |
Yes | 03 |
The debit couldn't be posted (for example, your balance dropped after Multigate checked it), or the balance couldn't be read. Nothing was paid. | None when the debit failed; transfer_ when the balance couldn't be read |
Check your balance, then send a new transfer. |
rejected |
Yes | 04 |
Declined by an approver in your business (nothing was debited), or by Multigate's compliance or settlement review. | None | Poll to detect it. If it was processing before, contact support about the debit. |
pending_ |
No | 43 |
Web-app transfers only, never transfers created with API credentials, and today only in the sandbox: Multigate's security check held the transfer for its compliance team. Nothing has been screened, approved or debited. Webhooks show it as pending. |
None | Nothing to do from the API. Released, it continues like a new transfer; otherwise it becomes cancelled. |
cancelled |
Yes | 04 |
A held web-app transfer that Multigate declined ("Security review declined") or didn't review within 24 hours ("Security review expired"). Nothing was debited. | None | Poll to detect it. |
responseCode and response give more detail. Failures reported by the bank or partner carry their own code, for example 26 ("Invalid Account Number").
If Multigate can't confirm what happened to a payment at the bank (for example the call to the bank was interrupted, or the bank accepted it and later reported it failed), the transfer stays pending or processing and its response reads "CBI provider dispatch outcome is unknown" (or "CBI reference belongs to another request; reconcile before resolution"). Multigate's operations team reconciles it with the bank; no webhook is sent until then. Don't send the payment again with a new unique: that could pay twice.
In the sandbox, Multigate checks pending payouts with the bank every 15 minutes (every 15 seconds in production, at first), so processing can last 15 minutes or more. Test banks and simulated partners decide the outcome, sometimes at random. Test that you handle every status rather than expecting one. See Sandbox vs production.
rejected never sends a webhook, and some failures (a compliance decline, code 43; a debit that couldn't be posted; a Nigerian account number that isn't 10 digits) end in failed without one. Poll any transfer that is still non-final in your system after the time you expect, with Find transfers.
Once a transfer is processing, Multigate keeps asking the bank for the result:
| Age of the transfer | Production checks |
|---|---|
| First 30 minutes | Every 15 seconds |
| 30 minutes to 6 hours | Every 15 minutes |
| 6 to 24 hours | Every 30 minutes |
Fees
- On top of the amount. The beneficiary receives the full
amount; your account is debitedamount + charge. The fee is in the source currency. - Quoted before, charged after. Get a transfer fee quotes it; the create response shows it as
charge. It is debited with the amount, after screening and approval. - Separate fee account. If Multigate has set up a separate fee account for your account, the fee is taken from there, and that account must cover it ("Insufficient funds in fee account" otherwise).
- Between Multigate accounts. A transfer to another Multigate account is free, unless the receiving business has opted in to a fee. It skips screening and approval, and completes as soon as Multigate's ledger records it.
- On your statement the fee is a separate
Feeline (IDFEE-…) with youruniqueasRequest Id custom. See Balances and statements.Reference - SWIFT bank charges are separate from Multigate's fee:
chargeTypedecides who pays the banks along the way.
Which rail is used
If you don't set paymentMethod, Multigate chooses:
flowchart TD
start[Transfer] --> chosen{paymentMethod set?}
chosen -->|Yes| use[Use it, if it is configured for the currency]
chosen -->|No| linked{Source is a SWIFT linked account?}
linked -->|Yes| swift[SWIFT]
linked -->|No| partner{Currency paid out through a payout partner?}
partner -->|Yes| prail[The partner's local rail, for example SEPA for EUR or FPS for GBP, otherwise SWIFT]
partner -->|No| lcy{NGN, GHS, ETB, CNY or XOF?}
lcy -->|Yes| local[The currency's configured local rail, NIP for NGN]
lcy -->|No| other[The currency's configured rail, otherwise SWIFT]
local --> big{NGN above 100,000,000?}
big -->|Yes| split[Split into several NIP payments]
- Amounts above the currency's maximum are refused ("Maximum amount exceeded"), except NGN on NIP, which is split into at most 100 parts (larger amounts are refused). List payment methods shows the configured limits and indicative fees.
- Single transfers don't take a purpose code. Purpose codes are used in bulk payout files.
- The transfer object doesn't show which rail was used.
Large NGN transfers are split
NGN transfers above the per-payment limit of the local instant rail (100,000,000 NGN unless Multigate configures otherwise) are sent as several payments. Validate a transfer tells you in advance (splitPreview), with the exact total fee.
When a transfer is split:
- The response includes
splitSummary, listing each part with its ownreference(transfer ID) andunique: yours withRequest Id -1,-2, … added. No part is stored under your originalunique, so look parts up withRequest Id ?unique. Your original ID stays reserved: sending it again is refused with "Unique request ID already exist".Request Id=<yours>-1,<yours>-2 - The fee is quoted once for the whole amount and shared across the parts in proportion to their amounts.
- At most 100 parts: an NGN transfer above 100 times the limit (10,000,000,000 NGN at the default limit) is refused with
400"Maximum amount exceeded". - Each part is its own transfer: it appears separately in Find transfers and sends its own webhook.
- Your transfer is complete when every part is
completed. Parts can end differently; handle a partly paid transfer.
Approvals
If your business uses maker-checker approval, a transfer starts in pending_ unless the API user that creates it may also approve it:
- Without an approval workflow: a transfer goes straight to screening and payment only when the API user has both the initiate payment and the approve payment permissions. Otherwise it waits for an approver.
- With an approval workflow (rules set up in the dashboard, by amount and currency): it goes straight through only when the API user is an approver for that amount and currency and the workflow needs one approval ("first to approve"). Otherwise it waits for the approvers the workflow names.
An approver approves or declines it in the Multigate dashboard:
- Approved: the transfer moves to
processing(responseCode05), is screened, then pays as normal. Your balance is checked just before the debit. - Declined: it moves to
rejected(responseCode04, no webhook) and the creator gets a "Transaction Declined" email with the reason.
To send payments straight to processing, ask your administrator to give your API user both the initiate and the approve payment permissions, or to make it an approver in your workflow.
Errors
NGN account numbers must be exactly 10 digits (NUBAN). Today a transfer to a shorter or longer number is accepted, then fails without a webhook, and the debit isn't reversed automatically. Validate the length in your system, and use Look up an account name, before creating the transfer.
A 400 on create means nothing was created. Fix the request and send it again (you can keep the same unique, because the first attempt didn't happen). The error body is [{ "errors": { "message": "…" } }], or for a missing field a failed_ body with an errors list (see Errors).
| Message | Cause | Fix |
|---|---|---|
| Unique request ID already exist | This unique was used before, by you or by another Multigate client. |
Look up the existing transfer. For a new payment, use a new ID with your own prefix. |
| Insufficient Balance | The account can't cover amount plus fee. Only from Validate a transfer, and for a large NGN transfer that is split, a linked-account source or a receivable payout; other transfers are accepted and end failed with responseCode 22. See When your balance is checked. |
Fund the account, or send less. |
| Insufficient funds in fee account | Your separate fee account can't cover the fee. Same timing as "Insufficient Balance". | Fund the fee account. |
| Source Account Not Found / Account not ready | Wrong account number or currency, or the account isn't active yet. | Check the account number, currency and status with List accounts. |
| Invalid Account Type | The account found can't be debited (it isn't a wallet or a linked account). | Use your wallet's account number, or send its accountId as source. |
| Invalid Source Account Owner | The account belongs to another business or customer. | Use your account, or set onBehalfOf to the customer that owns it. |
| Invalid onBehalfOf / Invalid sub onBehalfOf | The customer isn't yours, or isn't active (yet). | Check the customer ID and status. |
| Beneficiary Not Found / Beneficiary account not ready | Wrong beneficiaryId, or the saved beneficiary isn't active. |
Check the beneficiary, or send beneficiary. |
| Invalid Destination Account Owner | The saved beneficiary belongs to a different business or customer than the source account. | Use a beneficiary saved for the same business or customer. |
| Bank not found | Unknown bank code or BIC. | Check with List banks. |
| Currency mismatch | The beneficiary's currency differs from the source currency. | Convert first, then pay from the other account. |
| Source and Destination account cannot be the same | You're paying the source account itself. | Check the beneficiary account number. |
| Minimum amount not met / Maximum amount exceeded | Outside the limits for this currency. | Check List payment methods. |
| Supporting documents are required for 3rd party USD transfers. | USD payment to someone whose name doesn't match your account name. | Upload the documents and pass their IDs in assets. |
| failed_validation | A field is missing or invalid. | Read the errors list: it names each field. |
Unable to determine the fee… (HTTP 503) |
The fee service was briefly unavailable. Nothing was created. | Retry with the same unique. |
The full list is in Create a transfer.
When your balance is checked
So that creating a transfer is fast, Multigate doesn't check your balance when you create most transfers. It checks it just before it debits your account, after screening and approval:
| Transfer | Balance checked | If the balance is short |
|---|---|---|
| Most transfers from a wallet | When the payout starts | The transfer is created (200), then ends failed with responseCode 22 before any money moves. You receive transfer / transfer_ and the failure email. |
| NGN transfers above 100,000,000 (split), payouts from a linked account, receivable payouts | On create | 400 "Insufficient Balance" and nothing is created. |
What this means for you:
- Check before you send when you need an immediate answer, for example to tell a user "insufficient funds" on screen. Validate a transfer always checks the balance (amount plus fee).
- Handle
transfer_withfailed responseCode22as "not paid, nothing debited". Today itsresponseis the raw keyMessage insufficient(orBalance insufficient), not a sentence, so match on the code. Fund the account, then send a new transfer with a newFee Balance unique: the failed one keeps its ID.Request Id - The balance isn't reserved between the check and the debit. Several payouts sent at once from the same account can each pass the check while together they need more than the balance, so keep your own running total of what you've sent.
Common mistakes
- A new
uniqueon retry. After a timeout, look the payout up first. A new ID can pay twice.Request Id - Treating the create response as the result.
200means "accepted", not "paid". Wait for the webhook or poll. - Relying on webhooks alone.
rejectedand some failures send none. Poll non-final payouts. - Sending both
beneficiaryIdandbeneficiary. Send one.Details - Paying in a different currency from the source account. Convert first.
- Parsing amounts as floats. Amounts are decimal strings in responses; use a decimal type.
- Using a BIC for a local NGN payout, or a local code abroad. Local payouts take the bank code; international ones take the BIC.
Webhooks and emails
| Event | When |
|---|---|
Webhook transfer / transfer_ |
The transfer (or each split part) is paid. |
Webhook transfer / transfer_ |
It fails at the bank or partner, or your balance is short when the payout starts (responseCode 22). Not sent for compliance declines, failed debits or rejected transfers. |
| Email "Transfer Notification" to the API user that created it | When it starts processing, and when it completes or fails. |
| Email "Transaction Declined" to the creator | An approver declines it. |
Next
Pay hundreds of beneficiaries from one file.
Payload of every transfer event.