Skip to content
MultigateAPI
Support Staff

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.
uniqueRequestId 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_URL and MG_API_KEY are set in your terminal. If a call returns 401, check that MG_API_KEY holds 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_initiate_payment or can_approve_payment in permissions of the current user). Without either, create calls return 403. 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 (fundingDetails[].accountNumber) as sourceAccountNumber, or its accountId as sourceAccountId. The Quickstart saved an NGN one in 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 (entityId from Create a customer), and the source account belongs to that customer (List accounts with onBehalfOf). 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:

  1. 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 400 and nothing is created.
  2. Approval (if your business uses it). The transfer waits for an approver in your business. See Approvals.
  3. Screening. Multigate checks the payment against its compliance and risk rules. Most payments clear in seconds; some are held for manual review.
  4. 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 (responseCode 22) with a transfer_failed webhook, and nothing is debited. Your balance isn't reserved before this point.
  5. Payment. The money is sent to the beneficiary's bank on the right rail.
  6. Result. You receive a transfer_completed or transfer_failed webhook.
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 sourceCurrencyIso3Code Beneficiary beneficiaryDetails.bankBicOrCode Extra fields Example in Send a payout
Pay a Nigerian bank account in naira NGN Full beneficiaryDetails Local bank code from List banks (countryIso3Code=NGA) accountNumber must be the 10-digit NUBAN NGN local tab
Pay a local account in GHS, XOF or ETB That currency Full beneficiaryDetails Local bank code for the beneficiary's country countryIso3Code must be the bank's country Other local currency tab
Pay abroad in USD, EUR, GBP… That currency Full beneficiaryDetails The bank's BIC postCode; chargeType for SWIFT; intermediaryBank 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
uniqueRequestId 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.
sourceCurrencyIso3Code Yes The source account's currency, upper case (NGN).
sourceAccountNumber One of these two An account number of your source account, from List accounts.
sourceAccountId The source account's accountId, from List accounts.
beneficiaryDetails 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.entityId from Create a beneficiary, or beneficiaryDetails.entityId 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.
clientReference No Your own reference, stored with the transfer.

beneficiaryDetails needs name, accountName, accountNumber, bankBicOrCode, currencyIso3Code, countryIso3Code, addressLine1, city and state. The full list, with lengths, is in Create a transfer.

Send a payout

Check who you're paying

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.

Show the fee
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 splitPreview.projectedTotalFee is the exact fee for a split transfer.

Generate and store your uniqueRequestId

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.

Create the transfer

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

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 countryIso3Code=GHA). Here MG_GHS_ACCOUNT_NUMBER 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
  • countryIso3Code must be the bank's country: local bank codes repeat across countries.
  • 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_USD_ACCOUNT_NUMBER).

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
  • bankBicOrCode is the BIC. It must be in Multigate's bank list; an unlisted 11-character BIC is matched on its first 8 characters.
  • accountNumber is the account number or IBAN, letters and digits only.
  • intermediaryBank is 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 are bankName, bic and countryIso3Code; transfer webhooks return it as bankName, bankCode and swiftCode.
  • assets is required for USD only when beneficiaryDetails.accountName doesn't match your source account's name (a third-party payment). Leave it out when paying your own account abroad.
  • 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 processing until then.

Pay a beneficiary you saved earlier. Put its ID in MG_BENEFICIARY_ID: beneficiary.entityId from Create a beneficiary, or beneficiaryDetails.entityId 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 beneficiaryId or beneficiaryDetails, never both: both together fail validation.
  • 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_CUSTOMER_ID is the customer's entityId from Create a customer; MG_CUSTOMER_ACCOUNT_NUMBER is an account number from List accounts called with onBehalfOf=$MG_CUSTOMER_ID.

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 beneficiaryId must also be the customer's (saved with subClientId).
  • 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; onBehalfOf in 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.
beneficiaryDetails.entityId The saved beneficiary. Send it as beneficiaryId next time.

sourceAccountNumber in the response can differ from the number you sent: it shows the account Multigate debits. beneficiaryId is null when you sent beneficiaryDetails.

After you send

The create response is never the final result. Follow these steps for every payout.

Record the first status

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_approval 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.

Handle the webhook

When the payout finishes, Multigate sends transfer / transfer_completed or transfer / transfer_failed 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_completed means the beneficiary has been paid. Mark it paid.
  • transfer_failed means it wasn't paid; for a bank or partner failure the debit is reversed to your account. Show responseMessage to your user. To try again, create a new transfer with a new uniqueRequestId.
  • Payload fields are in the Event catalogue.
Poll when no webhook arrives

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.

If the create call itself gets no answer

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.

  1. Don't send a new transfer with a new uniqueRequestId: that can pay twice.
  2. Look the payout up with the poll command above (?uniqueRequestId=<yours>,<yours>-1).
  3. Found: the transfer exists. Save its transactionId and continue from step 1.
  4. Not found: wait a minute and look again. Still not found: send the same request again with the same uniqueRequestId. If the first attempt was created in the meantime, you get 400 "Unique request ID already exist" and nothing is paid twice; look it up again.
  5. If the same uniqueRequestId is sent twice at the same moment (for example by two of your servers), only one transfer is created and the other request gets 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.

Give your user a receipt

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_approval 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_completed Mark it paid.
failed Yes Varies Not paid. For a bank or partner failure the debit is reversed to your account. transfer_failed, with the exceptions below Read responseMessage. 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_failed Fund the account, then send a new transfer with a new uniqueRequestId.
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_failed 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_security_review 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 responseMessage 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 responseMessage 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 uniqueRequestId: that could pay twice.

Sandbox: slower, and sometimes random

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.

Some final outcomes send no webhook

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 debited amount + 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 Fee line (ID FEE-…) with your uniqueRequestId as customReference. See Balances and statements.
  • SWIFT bank charges are separate from Multigate's fee: chargeType decides 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 own reference (transfer ID) and uniqueRequestId: yours with -1, -2, … added. No part is stored under your original uniqueRequestId, so look parts up with ?uniqueRequestId=<yours>-1,<yours>-2. Your original ID stays reserved: sending it again is refused with "Unique request ID already exist".
  • 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_approval 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 (responseCode 05), is screened, then pays as normal. Your balance is checked just before the debit.
  • Declined: it moves to rejected (responseCode 04, 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

Check Nigerian account numbers before you send

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 uniqueRequestId, because the first attempt didn't happen). The error body is [{ "errors": { "message": "…" } }], or for a missing field a failed_validation body with an errors list (see Errors).

Message Cause Fix
Unique request ID already exist This uniqueRequestId 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 sourceAccountId.
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 beneficiaryDetails.
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 uniqueRequestId.

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_failed 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_failed with responseCode 22 as "not paid, nothing debited". Today its responseMessage is the raw key insufficientBalance (or insufficientFeeBalance), not a sentence, so match on the code. Fund the account, then send a new transfer with a new uniqueRequestId: the failed one keeps its 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 uniqueRequestId on retry. After a timeout, look the payout up first. A new ID can pay twice.
  • Treating the create response as the result. 200 means "accepted", not "paid". Wait for the webhook or poll.
  • Relying on webhooks alone. rejected and some failures send none. Poll non-final payouts.
  • Sending both beneficiaryId and beneficiaryDetails. Send one.
  • 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_completed The transfer (or each split part) is paid.
Webhook transfer / transfer_failed 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.