Skip to main content
This is the transfers-specific integration guide. Converting currency adds one object and about six ways to get it wrong, all of which are avoidable by doing things in the right order.

The rule

If sourceCurrency and destinationCurrency differ, fxQuoteId is required. There is no implicit conversion and no spot rate — omit the quote and you get 409 fx_quote_required.

Order of operations

The destination comes before the quote, because a quote is bound to a destination. Resolving a new destination invalidates the quote you already hold.

What a quote binds to

Five things are fixed at quote time. Change any of them and the transfer is refused. So a quote is not “a rate for NGN→USD”. It is “this transfer, at this rate, once”.

Fixing either side

amountSide decides which amount you control and which the rate determines.
Send only the amount matching amountSide. Sending both, or the wrong one, is rejected — at the quote as a validation failure, at the transfer as transfer_amount_invalid. Paying an invoice in the recipient’s currency is almost always destination. Sweeping a fixed sum out of your own balance is source.

Expiry comes from the provider

expiresAt is set by the FX provider, not by a fixed Sakawave window. Read it; do not assume a duration.
A quote is also single-use: used becomes true when a transfer spends it. Expired and spent both surface as 409 fx_quote_unavailable, so the two are indistinguishable — which is another reason not to hold a quote across a user decision.

Both currencies must allow FX

Before quoting, three things must be true, and only two of them are checkable in advance:
  1. fxEnabled on the source currency.
  2. fxEnabled on the destination currency.
  3. An active FX corridor for that ordered pair.
The third is not exposed by any endpoint. NGN → USD being wired does not imply USD → NGN. You can precheck the first two from GET /v1/currencies; the corridor only reveals itself as 409 fx_corridor_unavailable.

Rounding

fx_quote_amount_too_small means the converted amount rounds to nothing on the destination side. It is easy to hit when the destination currency has fewer minor units than the source — ₦ to UGX, say, where UGX has none at all. Check the destination currency’s minorUnits before offering an amount, and set a sensible floor in your own UI rather than letting the provider refuse it.

A complete conversion

The quote response tells you what the other side will be:
Note the form: you sent "2500000", you got back "25000.00". Show destinationAmount and rate on the confirmation screen, and do not send either of them back.

USD and USDC

Dollars and USDC convert into each other between your own accounts, one for one. It is the same quote and transfer as any other conversion, with three differences:
  • The rate is always 1, and nothing is charged for it. Any fee is the one for an own-account transfer, shown in the preview.
  • USDC converts in whole cents. 1.23 USDC converts and 1.234 does not: an amount finer than a cent has no dollar equivalent, and is refused with 409 conversion_amount_not_whole_cents rather than rounded. Dollars always convert exactly.
  • Only between your own accounts, and only with a USDC account whose deposit addresses Sakawave issued. Any other USDC account is refused at preview with 409 accounts_held_separately. Paying a USDC address straight from a dollar balance is not a conversion; convert first, then send a wallet payout.
Converting into USDC needs the tier that may receive crypto deposits, and converting out of it the tier that may withdraw crypto; below either, the transfer is refused with 403, reason: "feature_not_available". The quote still carries expiresAt. Read it as you would any other.

Things that go wrong, and why

Almost all of them come from holding a quote too long or reusing one. Quote, preview, and transfer in a single sequence, and requote rather than reusing.