The rule
IfsourceCurrency 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.
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.
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:fxEnabledon the source currency.fxEnabledon the destination currency.- An active FX corridor for that ordered pair.
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
"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.23USDC converts and1.234does not: an amount finer than a cent has no dollar equivalent, and is refused with409 conversion_amount_not_whole_centsrather 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.
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.