Products
FX conversions
Convert money between two of your accounts in different currencies, at a rate you see and lock before any money moves. Use it to hold USD from NGN, or to fund a payout in a currency you don't hold yet.
In short:
- Get a quote: ask for a rate to receive an amount in the target currency. The quote is valid for about 60 seconds.
- Lock it: confirm the quote with the two accounts. The source account is debited and the target account credited, usually during the call.
- Confirm: check the lock response's
status, or wait for thetrade_webhook.completed
| Term | Meaning |
|---|---|
| Source currency / account | What you convert from: the account that is debited. |
| Target currency / account | What you convert to: the account that is credited. |
| Quote | A rate Multigate offers you, with an ID, valid until endTime. |
| Lock | Accepting a quote. This is the step that moves money. |
| Liquidity provider | The bank or partner Multigate buys the currency from. |
How it works
sequenceDiagram
autonumber
participant You as Your backend
participant MG as Multigate
You->>MG: GET /fx-rates/best-rate (source, target, amount to receive)
MG-->>You: Quote id, buyRate, sellRate, endTime (about 60 seconds)
You->>MG: POST /fx-rates/lock-rate (quote id, accounts, amount)
Note over MG: Check balance, debit source account,<br/>book with the liquidity provider,<br/>credit target account
MG-)You: trade / trade_completed (can arrive before the response)
MG-->>You: Conversion: status completed (or processing)
Note over MG,You: processing: trade_completed follows when the credit settles
Two calls, no separate fee: Multigate's margin is built into the rate.
Before you start
MG_BASE_URL,MG_API_KEYandMG_CLIENT_IDare set, as in the Quickstart.- One active account in each currency, both yours, or both the same customer's. You need their
accountIdvalues (not account numbers). Put them in variables:
curl -s "$MG_BASE_URL/client-service/api/v1/accounts?pageSize=50" \
-H "mp-api-key: $MG_API_KEY" \
| jq '.data[] | {accountId, currencyIso3Code, status}'
export MG_NGN_ACCOUNT_ID="accountId of your NGN account"
export MG_USD_ACCOUNT_ID="accountId of your USD account"
- The source account funded with more than the amount that will be debited. Check it with Get an account balance before you lock.
- The
entityIdheader set to your client ID on every conversion request. Without it, the service uses the business your API key belongs to. - Permissions: lock rates (
can_) to quote and lock; view rates (lock_ rates can_) or lock rates to read conversions.view_ rates - For a customer: pass
onBehalfOfwhen you get the quote. See Acting for customers.
Today, a quote or lock with onBehalfOf is refused with 400 "Invalid onBehalfOf id", even for your own active customers. Until Multigate fixes this, convert in your own business's accounts, or contact Multigate.
In production, locking a quote the source account can't cover returns 402 "Insufficient balance", and a failed debit returns 402 "The settlement has failed due to an issue with our banking partners." In the sandbox, neither stops the conversion: an unfunded lock succeeds. Handle 402 in your code anyway. See Sandbox vs production.
Convert
amount is what you want to receive, in the target currency: here, 1,000 USD paid for in NGN. Save the quote's id in MG_QUOTE_ID:
QUOTE=$(curl -s "$MG_BASE_URL/trade-service/api/v1/fx-rates/best-rate?sourceCurrency=NGN&targetCurrency=USD&amount=1000" \
-H "mp-api-key: $MG_API_KEY" \
-H "entityId: $MG_CLIENT_ID")
echo "$QUOTE"
export MG_QUOTE_ID=$(echo "$QUOTE" | jq -r .id)
{
"message": "The FX rate has been successfully fetched.",
"id": "FX-261010-150000482913",
"sourceCurrency": "NGN",
"targetCurrency": "USD",
"buyRate": 1530.5,
"sellRate": 0.0006533812479581836,
"validityPeriod": 60,
"startTime": "2026-10-10T15:00:00.000Z",
"endTime": "2026-10-10T15:01:00.000Z",
"createdAt": "2026-10-10T15:00:00.482Z",
"updatedAt": "2026-10-10T15:00:00.482Z"
}
buyRate: source units per one target unit (here, NGN per USD), to 6 decimal places. Show this one to your user.sellRate: target units per one source unit (1 / buyRate).- The quote expires at
endTime, usually about 60 seconds afterstartTime. Show a countdown and lock straight after the user confirms. - Every call creates a new quote. Getting another one doesn't cancel the first.
Store the quote ID in your database first: it is your retry key, and the webhook can arrive before the response. Then send the quote id, the two accounts and the same amount you quoted, before endTime. Add a reference, unique across Multigate, that you can look the conversion up by.
curl -s -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 @- <<EOF
{
"id": "$MG_QUOTE_ID",
"sourceAccountId": "$MG_NGN_ACCOUNT_ID",
"targetAccountId": "$MG_USD_ACCOUNT_ID",
"amount": 1000,
"reference": "acme_conv_$(date +%s)"
}
EOF
{
"message": "The FX rate has been successfully locked.",
"id": "FX-261010-150000482913",
"status": "completed",
"sourceCurrency": "NGN",
"sourceAmount": "1530500",
"targetCurrency": "USD",
"targetAmount": "1000",
"rate": "0.0006533812479581836",
"tradeReference": "acme_conv_1791640820",
"sourceAccountNumber": "1029384756",
"targetAccountNumber": "4410023391",
"valueDate": "2026-10-10T15:00:20.114Z",
"createdAt": "2026-10-10T15:00:20.114Z",
"updatedAt": "2026-10-10T15:00:21.902Z"
}
- The source account is debited
sourceAmount(2 decimal places); the target account receivestargetAmount. RecordsourceAmount: it is close toamount × buyRatebut not always equal. rateis the quote'ssellRate.- Amounts may be strings or numbers: see Amounts and currencies.
source,Account Number targetandAccount Number valueDateappear only in this response. Store them if you need them.
If status is completed, the target account is already credited. You may already have received trade / trade_ for it.
If it's processing, wait for trade_, or check with Get a conversion using the quote ID or your reference.
Conversion statuses
stateDiagram-v2
direction LR
[*] --> completed: credited straight away
[*] --> processing: debited, credit waiting for settlement
[*] --> awaiting_balance: post-funded, short of funds
[*] --> failed: provider refused at lock time
awaiting_balance --> processing: account funded and debited
processing --> completed: settled and credited
processing --> failed: provider failed (pairs priced through USD)
completed --> [*]
failed --> [*]
| Status | Final | Meaning |
|---|---|---|
completed |
Yes | The target account is credited. trade_ is sent. |
processing |
No | The source is debited; the credit is waiting for the liquidity provider or Multigate's settlement. trade_ follows. |
awaiting_ |
No | Only for post-funded accounts (agreed with Multigate): the account was short of funds, so the debit is deferred. Fund the account. |
failed |
Yes | The liquidity provider refused the conversion, usually when you locked it (the lock call returned 400 "Failed to execute trade, please reach out to support"). The source debit is reversed. |
Today, if the liquidity provider fails a processing conversion later, Multigate reverses the source debit but the conversion can still end as completed (or stay processing), and trade_ can still be sent. Get a conversion then shows the same status. Before you release money to your own customers based on a conversion that was processing, check that the target account received targetAmount (for example with Get an account balance or its statement).
Handling trade_completed
- Match the event to your conversion by
id, which is the quote ID.tradeis yourReference reference(or one Multigate generated). - Store the quote ID before you call lock: the event can arrive before the lock response.
- Read the amounts from your lock response or Get a conversion. For pairs priced through USD, an event sent after a
processingconversion settles can carry the amounts of one leg (to or from USD) and a Multigate-generatedtrade.Reference - Deduplicate by
Idempotency-Keyand by quote ID: the same conversion can be notified more than once.
Retrying safely
Lock has no idempotency key, but a quote can be locked only once. That makes the quote ID your retry key.
If a lock call times out, the connection drops, or you get a 5xx:
- Call Get a conversion with the quote ID. If it's found, that is your conversion: follow its status.
- If it isn't found, send the same lock request again (same quote ID, same
reference):409"Rate already locked": the first attempt is recorded or still running. Wait a few seconds and call Get a conversion again.400"Rate expired": no conversion was recorded for this quote. Check the source account balance for a debit you can't explain before you get a new quote, and contact support with the quote ID if you find one.200: converted.
- Never lock a new quote for the same purpose until you've confirmed the first one didn't go through.
503 "Failed to execute trade, please reach out to support" means Multigate couldn't reserve the quote: nothing was debited, so retry the same request shortly. The same message with 400 means the provider refused and the conversion failed, so always check the HTTP status, not just the message.
Get a conversion and List conversions only return conversions made for your own business. A conversion locked for a customer (onBehalfOf) returns 404. Track customer conversions with the trade_ webhook (it comes to you) and the customer's account balances.
Use a new reference for every conversion, including after a failed one: a reused value returns 409 "Duplicate reference". References must be unique across Multigate, so add your own prefix or use a UUID.
Which pairs you can convert
Pairs depend on the liquidity Multigate has configured. Get a quote to check a pair: an unavailable pair returns 404 (or 400) "Rates for this currency pair are currently unavailable."
- Pairs that include USD are quoted directly. Other pairs are priced through USD.
- USD to NGN, USD to GHS and USD to ETB are quoted for information only. Locking them returns
404"The rate is not currently tradeable". They exist because many treasury systems expect a buy and a sell rate for every pair: the rate is derived from the opposite direction (for example NGN to USD) with a margin, so you can display it, not trade on it.
Errors
| Message | Status | Call | Fix |
|---|---|---|---|
| Validation error body | 400 |
Both | Fix the field named in errors. |
| Rates for this currency pair are currently unavailable. | 400, 404 |
Quote | The pair isn't available right now. Try later or ask Multigate. |
| Invalid onBehalfOf id | 400, 404 |
Both | onBehalfOf isn't your customer, doesn't exist, or doesn't match the quote's customer. On lock, leave it out or send the quote's customer. |
| Unauthorized access | 403 |
Both | Your API user lacks the permission, or the entityId header isn't one of your businesses. |
| The rate could not be found. | 404 |
Lock | Unknown quote ID. |
| The rate is not currently tradeable | 404 |
Lock | The pair is quoted for information only. |
| Rate already locked | 409 |
Lock | The quote was already used, or a lock is in progress. Look it up with Get a conversion. |
| Rate expired | 400 |
Lock | Get a new quote and lock it before endTime. |
| Duplicate reference | 409 |
Lock | Send a new reference. |
| The customer viban could not be found. | 400 |
Lock | source or target doesn't exist. Use accountId values. |
| Invalid source or target account | 400 |
Lock | Accounts must be yours (or the quote's customer's) and match the quote currencies. |
| The multi-gate viban could not be found. | 400 |
Lock | The pair isn't set up on Multigate's side. Contact support. |
| Insufficient balance | 402 |
Lock | Production only. The available balance must be more than the debit. Fund the account. Nothing was converted. |
| The settlement has failed due to an issue with our banking partners. | 402 |
Lock | Production only. The debit didn't go through. Check the source balance before retrying: in rare cases a debit that timed out completes later. |
| Failed to execute trade, please reach out to support | 400 |
Lock | The liquidity provider refused. The conversion is failed and the debit was reversed. Get a new quote. |
| Reversal failed. Please try again later. | 417 |
Lock | The provider refused and the automatic refund failed: the money is still debited. Contact support with the quote ID. Don't retry. |
| Failed to execute trade, please reach out to support | 503 |
Lock | Multigate couldn't reserve the quote. Nothing was debited. Retry the same request. |
Next steps
- Payouts: pay out from the target account.
- Event catalogue: the
trade_payload.completed - Balances and statements: reconcile the debit and the credit.
- Cross-border payouts: convert, then pay a supplier.