Use cases
Cross-border payouts
Pay suppliers, contractors or partners abroad from your local-currency balance: convert NGN to USD at a locked rate, then pay out by international bank transfer (SWIFT, the network banks use to send money abroad).
Who it's for: corporate enterprises and SMEs paying suppliers abroad, and fintechs offering international payouts.
Architecture
flowchart LR
you[Your backend] -->|1 quote and lock| fx[FX conversion]
ngn[(Your NGN account)] -->|debited sourceAmount| fx
fx -->|credited targetAmount| usd[(Your USD account)]
you -->|2 create transfer| usd
usd -->|SWIFT payment| bank([Supplier's bank abroad])
fx -.->|trade_completed| hook[Your webhook endpoint]
usd -.->|transfer_completed or transfer_failed| hook
sequenceDiagram
autonumber
participant You as Your backend
participant MG as Multigate
participant Bank as Supplier's bank abroad
You->>MG: Get a transfer fee (USD account, supplier amount)
You->>MG: Get a quote NGN to USD (amount = supplier amount + fee)
MG-->>You: Quote id, buyRate, endTime (about 60 seconds)
You->>MG: Lock the rate (quote id, NGN accountId, USD accountId)
MG-->>You: status completed (or processing)
MG-)You: trade / trade_completed
You->>MG: Create a transfer from the USD account (SWIFT, chargeType, assets)
MG-->>You: 200, status pending (screening)
Note over MG: Screening, approval, debit, then payment
MG->>Bank: SWIFT payment
MG-)You: transfer / transfer_completed or transfer_failed
Before you start
- A sandbox API key, and the variables
MG_BASE_URL,MG_API_KEYandMG_CLIENT_IDset as in the Quickstart. - A webhook URL registered (Webhooks overview). Events that happen before you register it are never delivered.
- Your API user has the lock rates permission (to convert) and the initiate payment permission (to quote fees and pay). See Approvals for whether its payments also need an approver.
- Multigate has set up partner banks for NGN and USD for your business, and you have accepted their terms of service in the dashboard.
- Supporting documents. A USD payout to an account whose name doesn't match your own account name is a third-party payment and needs supporting documents, such as the supplier's invoice. Paying a supplier is almost always third-party. The upload endpoint isn't public yet: ask Multigate how to upload documents and get their IDs.
Today a quote or lock with onBehalfOf is refused with 400 "Invalid onBehalfOf id". If you pay out for your customers (sub-clients), convert in your own business's accounts until Multigate fixes this. See FX conversions.
Build plan
Open one account in each currency with Create an account, each with its own unique (your unique ID for the request, so a retry isn't executed twice: see Idempotency). Follow Accounts › Open an account.
From each response, store:
account.: the conversion needs it asaccount Id source(NGN) andAccount Id target(USD).Account Id - An account number for payouts: the wallet's own number or one of its
fundingvalues.Details[]. account Number
If an account comes back processing, wait for the account / account_ webhook. Some USD wallets have no virtual account (funding is empty); that's expected, because you fund them by conversion.
Fund the NGN account by bank transfer to its funding account number. In the sandbox, use Simulate an inflow.
Save the supplier once with Create a beneficiary, and keep beneficiary. as their beneficiaryId. For USD, bankBicOrCode is the bank's SWIFT BIC (an 8- or 11-character bank code such as COBADEFFXXX). Send the address, city, state and a real postCode (it defaults to 000000). See Beneficiaries › Rules by currency.
You can skip this step and send beneficiary on the transfer instead: Multigate then saves the beneficiary for you.
Multigate's fee is charged to the USD account on top of the amount, so the USD account must hold both. Quote it with Get a transfer fee:
curl "$MG_BASE_URL/payment-service/api/v1/transfer/get-transfer-fee?amount=18000&sourceCurrencyIso3Code=USD&sourceAccountNumber=4410023391&beneficiaryId=Bn4kQ8rT2vX6yZ1aC3dEf" \
-H "mp-api-key: $MG_API_KEY"
{ "message": "Successful", "feeAmount": 25, "feeCurrency": "USD", "isFeePercentage": false, "transactionCharge": 25, "beneficiaryWillReceive": 18000 }
The fee shown is an example; yours comes from your pricing. You need 18000 + feeAmount USD: here 18,025.
Get a quote with Get a quote, where amount is what you want to receive in USD, then lock it with Lock a rate before endTime (about 60 seconds). Both calls need the entityId header set to your client ID.
curl "$MG_BASE_URL/trade-service/api/v1/fx-rates/best-rate?sourceCurrency=NGN&targetCurrency=USD&amount=18025" \
-H "mp-api-key: $MG_API_KEY" \
-H "entityId: $MG_CLIENT_ID"
Show your user buyRate (NGN per USD), store the quote id, then lock:
curl -X POST "$MG_BASE_URL/trade-service/api/v1/fx-rates/lock-rate" \
-H "mp-api-key: $MG_API_KEY" \
-H "entityId: $MG_CLIENT_ID" \
-H "Content-Type: application/json" \
-d '{
"id": "FX-261010-150000482913",
"sourceAccountId": "Ac4mW8pZ2kR6tY1nQ7xVb",
"targetAccountId": "Ac7tY2nQ5xVb9mW3pZ8kR",
"amount": 18025,
"reference": "acme_conv_20261010_0007"
}'
statuscompleted: the USD account is credited. Go on.statusprocessing: wait fortrade/trade_, or poll Get a conversion with the quote ID.completed - Before paying out after a conversion that was
processing, check that the USD account actually receivedtargetAmount(Get an account balance): todaytrade_is also sent when the provider fails a conversion later.completed
Full rules, retries and errors: FX conversions.
Generate a unique, store it, then call Create a transfer from the USD account:
curl -X POST "$MG_BASE_URL/payment-service/api/v1/transfer/single-transfer" \
-H "mp-api-key: $MG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"uniqueRequestId": "acme_po_20261010_0003",
"reference": "Invoice LT-2026-118",
"amount": 18000,
"sourceCurrencyIso3Code": "USD",
"sourceAccountNumber": "4410023391",
"beneficiaryId": "Bn4kQ8rT2vX6yZ1aC3dEf",
"paymentMethod": "SWIFT",
"chargeType": "SHA",
"assets": ["3c9d7e21-5b4a-4f6e-8a1d-2e7f9c0b6a51"]
}'
assets: the IDs of the supporting documents (see Before you start). Without them a third-party USD payout is refused with400"Supporting documents are required for 3rd party USD transfers."chargeType: who pays the banks' SWIFT charges (table below).- With
beneficiaryinstead ofDetails beneficiaryId, addintermediary(Bank bankName,bic,country) when the supplier's bank needs a correspondent bank.Iso3 Code - Store
transactionId(PMT-…) with yourunique. The response status isRequest Id pending(being screened),pending_(waiting for your approver) orapproval processing.
If the call times out or returns 5xx, the outcome is unknown: follow Payouts › After you send (step "If the create call itself gets no answer") and never retry with a new unique.
Wait for transfer / transfer_ or transfer_, matched by unique. USD payouts are completed by Multigate's operations team, so they stay processing longer than local transfers: show "processing" in your UI. If one isn't final after 24 hours, contact support with its transactionId.
When it's completed, give the supplier the PDF from Download a transfer receipt.
Choose who pays SWIFT charges
chargeType |
Effect |
|---|---|
OUR |
You pay all charges; the supplier receives the full amount. |
SHA |
Charges are shared; intermediary banks may deduct some. The default when you leave chargeType out. |
BEN |
The supplier pays the charges. |
chargeType isn't validated, so send one of these values exactly. Multigate's own fee is separate and always charged to your account on top of the amount.
Data to store
| Field | From | Used for |
|---|---|---|
NGN and USD accountId |
Create an account | source and target on the lock |
| USD account number | the account's number or funding |
source on the fee quote and the transfer |
beneficiaryId |
Create a beneficiary (beneficiary.) |
Paying the same supplier again |
Quote id and your reference |
Get a quote, your own | Retrying a lock, matching trade_ |
sourceAmount, targetAmount, rate |
the lock response | Reconciling the conversion |
unique (stored before the call) and transactionId |
your own, then the transfer response | Matching webhooks, lookups, support |
Webhooks to handle
| Event | What to do |
|---|---|
trade / trade_ |
Match by id (the quote ID). It can arrive before the lock response. Check the USD balance before paying out after a processing conversion. |
transfer / transfer_ |
Mark the payout paid; send the receipt. |
transfer / transfer_ |
Mark it failed and show response. A new attempt needs a new unique. |
Some outcomes send no webhook: a decline by your approver (rejected), a compliance decline (failed, responseCode 43) and a debit that couldn't be posted (failed, 03). Poll Find transfers ?unique for payouts that stay non-final. See Outcomes without a webhook.
Reconciliation
Each day, pull Get an account statement for both accounts (Balances and statements):
- NGN account: a
Tradedebit ofsourceAmountper conversion. - USD account: a
Tradecredit oftargetAmountper conversion, and aTransferdebit per payout withcustom= yourReference unique, plus its fee.Request Id
Anything only in your records is usually a payout still in a non-final status; anything only in the statement is usually a webhook you didn't process.
Things that trip people up
- The transfer currency must match the source account: you can't pay USD from an NGN account. Convert first.
- The balance isn't reserved when you create the transfer; it's checked just before the debit. Keep the USD in the account until the payout is final: if it's short at that check, the payout ends
failed(22, with a webhook), and if it drops right after the check,failed(03) without one. - In the sandbox an unfunded lock succeeds; production refuses it with
402"Insufficient balance". Check the NGN balance before you lock. See Sandbox vs production. - Never lock a second quote for the same payout until you've confirmed the first didn't go through: a quote ID is the only retry key for a conversion.
Go-live notes
- Agree USD payout pricing and the supporting-document process with Multigate.
- Decide with your admin who approves payouts (maker-checker: one person creates, another approves), and test the
pending_path.approval - Re-test an FX lock larger than your balance in production on day one and handle the
402. - Work through the go-live checklist.