Moja SwitchDocs Sandbox live

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

bash
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) or receive_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-in fx_rate, expires_at, a cost disclosure and fx_provenance (where the number came from, for your records and your auditors).
  • Show your end user only send_amount, receive_amount and fx_rate. The margin is already inside fx_rate. fees.fx_markup and fx_provenance are for your systems, never for an end user.

A quote is single-use and valid until expires_at. Pay on it:

json
{
  "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:

json
"fx": {
  "rate": "1850.25",
  "receive_amount": { "value": "185025.00", "currency": "TZS" }
}
  • fx.receive_amount is authoritative: it is paid verbatim.
  • fx.rate is the declared basis, checked for consistency: |receive_amount − round(send_amount × rate)| ≤ 1 minor unit, else 422 fx_inconsistent.
  • Production requires fx on every quote and payment (422 fx_required).
  • Responses echo your fx_rate and receive_amount. Moja Switch adds no margin.
  • In the sandbox only, omitting fx on 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.

bash
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.