Guide
FX quotes and Moja FX
The two FX models, how a quote is priced and locked, and what the reference rate is (and is not).
Every cross-currency payment has an exchange rate. Who sets it is a property of your tenant, not of an individual request: Moja Switch configures your tenant with one of two FX models.
| Moja Switch FX | BYO-FX | |
|---|---|---|
| Who sets the customer rate | Moja Switch prices it from the Moja FX reference rate, plus your own margin for the corridor | You do, from your own FX source |
What you send to POST /quotes |
send_amount or receive_amount; no fx |
send_amount and an inline fx |
| How you pay | POST /payments with the quote's quote_id; no fx |
POST /payments with an inline fx |
| Margin added by Moja Switch | Your agreed margin, inside fx_rate |
None |
Moja FX is a reference-rate service. Moja Switch serves a validated reference rate and applies your commercial terms to it. It is not the FX counterparty: conversion is executed through Moja Switch's banking and liquidity partners.
Moja Switch FX: quote, then pay on the quote
curl -sS -X POST "$MOJA_BASE/quotes" \
-H "Authorization: Bearer $MOJA_KEY" \
-H 'Content-Type: application/json' \
-d '{
"corridor": "CA-TZ",
"send_amount": { "value": "100.00", "currency": "CAD" },
"receive_currency": "TZS",
"payout_method": "MOBILE_MONEY"
}'- Send exactly one of
send_amount(what the sender pays) orreceive_amount(what the recipient must get). - Do not send
fx, even empty:422 fx_not_accepted. - The response returns
pricing_mode: platform_priced, the all-infx_rate,expires_at, a costdisclosureandfx_provenance(where the number came from, for your records and your auditors). - Show your end user only
send_amount,receive_amountandfx_rate. The margin is already insidefx_rate.fees.fx_markupandfx_provenanceare for your systems, never for an end user.
A quote is single-use and valid until expires_at. Pay on it:
{
"quote_id": "quo_…",
"corridor": "CA-TZ",
"reference": "partner-tx-12345",
"send_amount": { "value": "100.00", "currency": "CAD" },
"…": "sender, payout, purpose, sender_kyc"
}Moja Switch locks that quote's price exactly; it never re-prices. A used quote is
422 quote_invalid, an expired one 422 quote_expired, and a payment without a
quote_id 422 quote_required. Re-quote rather than extending a quote yourself.
When there is no rate
If the reference rate is stale, unvalidated or outside its tolerance band, the
quote is refused with 503 fx_unavailable. This is an expected state, not an
incident. Show a "no price available" state and retry later. Never fall back
to a cached or indicative rate: a displayed rate is a rate your customer expects.
Other Moja Switch FX refusals: 422 commercial_terms_not_configured (no margin
configured for you on this corridor; there is no default),
422 platform_fee_terms_not_configured, 422 amount_below_minimum.
BYO-FX: you bring the rate
Send an inline fx with both the rate and the exact amount to pay out:
"fx": {
"rate": "1850.25",
"receive_amount": { "value": "185025.00", "currency": "TZS" }
}fx.receive_amountis authoritative: it is paid verbatim.fx.rateis the declared basis, checked for consistency:|receive_amount − round(send_amount × rate)| ≤ 1minor unit, else422 fx_inconsistent.- Production requires
fxon every quote and payment (422 fx_required). - Responses echo your
fx_rateandreceive_amount. Moja Switch adds no margin. - In the sandbox only, omitting
fxon a corridor with a sandbox rate (CA-TZ) prices from a sandbox rate as a development convenience. It never happens in production.
The reference rate endpoint
GET /fx/reference?corridor=CA-TZ returns the indicative reference rate for
a corridor you are entitled to: receive-currency units per one send-currency
unit, as a decimal string.
curl -sS "$MOJA_BASE/fx/reference?corridor=CA-TZ" \
-H "Authorization: Bearer $MOJA_KEY"It is never a customer rate: no margin, no quote, no lock. Do not show it as
the rate a customer will get. It fails closed with 503 fx_unavailable rather
than return a stale value. To price a payment, use POST /quotes.
Amounts
Amounts are decimal strings in the currency's own precision. Read each currency's
minor_unit_exponent from
GET /corridors/{corridor_code};
never assume two decimal places. Let the server do cross-currency arithmetic: a
quote returns both sides.
