Capay Global

FX rates and quotes

Read the exchange rates Capay Global treasury has approved for your environment, and lock one into a time-boxed quote before you request a settlement. A quote informs and locks; it does not convert money.

What a quote is, and is not

Collections land in the local currency of your customers. Settlement to you is arranged by hand with your account manager (see Fees & settlement): a crypto settlement buys the asset at a rate, and that rate is what a quote locks. Every rate has a mid (the treasury reference) and a spread in basis points applied against you; the rate you see is already net of the spread, and amounts round down on the way out because you are the one being paid.

A quote is valid for quote_ttl_seconds (15 minutes by default) and applies only if the settlement payout is CREATED before expires_at. Share the quote id (fxq_...) with your account manager when you request settlement; once a payout consumes it the quote reads used and names the payout in used_by. A quote that is not used simply expires.

  • It does not convert a balance: no ledger journal is posted, no money moves, nothing is reserved.
  • It does not create a payout, and the API offers no cross-currency payout: POST /v1/payouts is same-currency only.
  • It is not a market feed: rates are approved by two treasury admins (maker-checker), and quoting pauses when a rate is older than the freshness window (72 hours by default) until it is re-approved; a paused rate is not listed either, so GET /v1/fx/rates and POST /v1/fx/quotes always agree.
  • Every object carries conversion: "manual_settlement_only" so a client can never mistake it for an automatic conversion.

List approved rates

GET/v1/fx/rates

The active rates visible to the environment of your key. Pass both base and quote to read one pair in that orientation; pass neither to list every active pair as stored. No scope beyond a valid key, like GET /v1/pricing.

In the sandbox the rates are deterministic, deliberately non-market fixtures (fixture: true, for example 600 XAF per USD) so you can integrate without waiting for treasury. Never display a fixture rate to a customer as a real rate.

Query parameters

ParameterTypeDescription
baseoptionalstring3-letter ISO code of the currency you convert from, e.g. XAF. Both or neither of base and quote.
quoteoptionalstring3-letter ISO code of the currency you convert to, e.g. USD.

Example request

curl
curl "https://api.capayglobal.com/v1/fx/rates?base=XAF&quote=USD" \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa"

Example response

200 response
{
  "status": "success",
  "data": {
    "object": "list",
    "data": [
      {
        "object": "fx_rate",
        "id": "fxr_7Hq2mZk9Xa41Lp0Bc3Vn8Rt2",
        "base": "XAF",
        "quote": "USD",
        "mid": "0.001666666667",
        "spread_bps": 100,
        "rate": "0.00165",
        "source": "admin_manual",
        "valid_from": "2026-09-04T08:00:00.000Z",
        "valid_to": null,
        "as_of": "2026-09-04T08:00:00.000Z",
        "quote_ttl_seconds": 900,
        "livemode": false,
        "fixture": true,
        "conversion": "manual_settlement_only"
      }
    ],
    "has_more": false
  },
  "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}

Each fx_rate carries:

FieldTypeDescription
idstringRate id, prefixed fxr_. A quote names the rate it locked in rate_id.
base / quotestringThe pair, in the orientation you asked for (or as stored when listing).
midstringUnits of quote per 1 base before the spread, a decimal string with up to 12 decimals.
spread_bpsintegerBasis points applied against you.
ratestringWhat you receive per 1 base after the spread. This is the number a quote locks.
sourcestringtreasury (entered by treasury), market (promoted from a market observation, still approved by two admins) or provider (reserved). The counterparty is never named.
as_ofstringWhen treasury approved the rate. Past the freshness window the rate is neither listed nor quotable until re-approval.
quote_ttl_secondsintegerHow long a quote created now stays open.
fixturebooleantrue for the sandbox fixtures.
conversionstringAlways "manual_settlement_only".

Create a quote

POST/v1/fx/quotes

Locks the approved rate for amount of currency into to_currency. Requires the fx:write scope. Idempotency-Key is optional here (a quote moves no money); when you send one, the same key replays the stored quote exactly like the money endpoints.

The open-quote cap is 60 per environment; beyond it the endpoint answers 429 rate_limited with a Retry-After until some expire or are used.

Body fields

ParameterTypeDescription
amountrequiredstringDecimal string in currency. Zero-decimal currencies such as XAF take whole numbers only.
currencyrequiredstringThe currency you settle from (3-letter ISO), e.g. XAF.
to_currencyrequiredstringThe currency you receive in (3-letter ISO), e.g. USD. USDT and USDC settlements are filed as USD.
referenceoptionalstringYour own label, up to 160 characters, echoed back as reference.

Example request

curl
curl -X POST https://api.capayglobal.com/v1/fx/quotes \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "1000000",
    "currency": "XAF",
    "to_currency": "USD",
    "reference": "settle-sep-w1"
  }'

Example response

201 response
{
  "status": "success",
  "data": {
    "object": "fx_quote",
    "id": "fxq_3kP9tW2yNq7Lm4Hd8Sb1Vc6Z",
    "pair": "XAF/USD",
    "currency_in": "XAF",
    "amount_in": "1000000",
    "currency_out": "USD",
    "amount_out": "1650.00",
    "mid": "0.001666666667",
    "spread_bps": 100,
    "rate": "0.00165",
    "fee": "16.66",
    "fee_currency": "USD",
    "status": "open",
    "expires_at": "2026-09-04T08:15:00Z",
    "created_at": "2026-09-04T08:00:00.000Z",
    "reference": "settle-sep-w1",
    "rate_id": "fxr_7Hq2mZk9Xa41Lp0Bc3Vn8Rt2",
    "used_by": null,
    "livemode": false,
    "fixture": true,
    "signature": "b3f1c2d4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2",
    "conversion": "manual_settlement_only"
  },
  "request_id": "9b2c1a08-3e44-4f6d-8a17-c05e9f21d3b6"
}

Worked example: 1,000,000 XAF at mid 0.001666666667 is 1666.66 USD gross; minus 100 bps the locked rate is 0.00165 and you receive 1650.00 USD, fee 16.66 USD. amount_out floors to the cent; the difference is never rounded in your disfavour beyond that.

The fx_quote object:

FieldTypeDescription
idstringQuote id, prefixed fxq_. This is what you share with your account manager.
currency_in / amount_instringWhat you settle, as sent.
currency_out / amount_outstringWhat you receive, rounded DOWN to the minor unit of currency_out.
mid, spread_bps, ratestring / integerThe locked terms, copied from the rate at creation.
fee / fee_currencystringThe spread expressed in currency_out (gross at mid minus amount_out).
statusstringopen, used, expired or void. expired is derived at read time (an open quote past expires_at). void is reserved: nothing voids a quote yet; a quote you do not use simply expires.
expires_atstringISO 8601, second precision. The settlement payout must be created before this instant.
rate_idstringThe fxr_ rate the quote locked.
used_byobject | null{kind: "settlement_payout", id: "spo_...", used_at} once a payout consumed the quote.
signaturestringHMAC-SHA256 (hex) over the quote terms, verifiable by Capay Global when the quote is used. See Signature below.
fixture / livemodebooleanSandbox fixture flag and environment.

Read quotes

GET/v1/fx/quotes/{id}
GET/v1/fx/quotes

GET /v1/fx/quotes/{id} returns one quote by fxq_ id (404 resource_not_found when it is not yours or not in this environment). GET /v1/fx/quotes lists your quotes newest first with keyset pagination (limit, starting_after) and an optional status filter. Both require fx:read.

Query parameters

ParameterTypeDescription
statusoptionalstringopen, used, expired or void. open excludes expired quotes; expired is derived from expires_at.
limitoptionalinteger1 to 100, default 10.
starting_afteroptionalstringThe id of the last quote of the previous page.

Example request

curl
curl https://api.capayglobal.com/v1/fx/quotes/fxq_3kP9tW2yNq7Lm4Hd8Sb1Vc6Z \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa"
curl
curl "https://api.capayglobal.com/v1/fx/quotes?status=used&limit=10" \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa"

Example response

200 response (used quote, abridged)
{
  "status": "success",
  "data": {
    "object": "fx_quote",
    "id": "fxq_3kP9tW2yNq7Lm4Hd8Sb1Vc6Z",
    "status": "used",
    "used_by": {
      "kind": "settlement_payout",
      "id": "spo_9Lm4Hd8Sb1Vc6Z2kP9tW2yN",
      "used_at": "2026-09-04T08:05:12.000Z"
    },
    "conversion": "manual_settlement_only"
  },
  "request_id": "0c3d2b19-4f55-4a7e-9b28-d16fa032e4c7"
}

Lifecycle

open -> used when a settlement payout is created against it before expires_at, with the exact amount_in in currency_in and the matching asset; the quote then names the payout (spo_...) in used_by and you see the same id in the portal under Settlements.

open -> expired when expires_at passes unused. Create a new quote; there is no extension.

A used quote stays used even if the payout is later cancelled: the audit trail is never rewritten, and ops locks a new quote for the retry. A partial settlement also needs a new quote: amount_in must match the payout exactly.

Signature

signature is an HMAC-SHA256 over id, currency_in, amount_in (minor units), currency_out, amount_out (minor units), rate and expires_at, keyed with a server secret. It makes a quote tamper-evident on our side: a quote whose terms no longer match its signature is refused at settlement. It is not a public-key signature you can verify offline; to confirm the terms of a quote, read it back with GET /v1/fx/quotes/{id}. An asymmetric scheme with published keys is on the roadmap.

Sandbox

Sandbox keys always find nine fixture pairs (USD against XAF, XOF, NGN, GHS, KES, UGX, TZS, RWF and ZAR at round, non-market mids with a 100 bps spread) so quoting works on day one; fixtures cannot be expired or replaced. Production keys see every pair treasury has approved for the environment, not only the currencies you hold; a pair with no approved, fresh rate answers 503 fx_rate_unavailable, and a pair that is not offered at all answers 422 fx_pair_unsupported.

Errors

Beyond the shared catalog (invalid_api_key, validation_error, rate_limited, internal_error), these endpoints return:

CodeHTTPWhen it is returned
fx_pair_unsupported422No rate is offered for this currency pair (only when both currencies are given).
fx_rate_unavailable503The pair is offered but no approved, fresh rate exists right now (none approved, or the approved rate is older than the freshness window). Retry later; GET /v1/fx/rates lists exactly what can be quoted.
validation_error422Malformed body or query: a fraction on a zero-decimal currency, same currency twice, an amount that converts to less than one minor unit, a reference over 160 characters.
insufficient_scope403The key lacks fx:write (create) or fx:read (read). Agent keys cannot quote at all.
resource_not_found404The quote id does not belong to this merchant and environment.
rate_limited42960 open quotes already exist in this environment, or the app quota is exhausted.