Exchange between your own accounts
/fx/exchangesTwo forms, exactly one per request:
quoteId— executes the quote at its locked rate; every term comes from the quote, so send nothing else. An expired, already used or unknown quote is400 fx_quote_expired.sellAmount+buyCurrency— exchanges at the rate of the moment, priced and executed in the same request (as the legacyPOST /v1/exchangedid). The rate you got is in the response. Accounts resolve as onPOST /fx/quotes, with the same codes.
Always 202. A fiat exchange completes inside the request and normally reads completed; a crypto one
settles on a venue and reads processing until it does — poll Location. A refusal at execution
(insufficient_funds, limit_exceeded) is a 400 with its own code; the exchange stays failed and keeps
its merchantReference, so a new attempt needs a new reference as well as a new key. There is no approval
step for API exchanges. Requires fx:write.
A 500 is never a refusal. It means the outcome is not known yet — retry with the same
Idempotency-Key and you get the exchange as it stands, never a second one. An Idempotency-Key already
used for something other than this API key's exchange is 400 idempotency_key_reused.
Parameters
Idempotency-KeystringheaderrequiredClient-generated unique key for this logical request (UUID recommended). See Idempotency.min length 1 · max length 255
X-Request-IdstringheaderOptional client correlation id; echoed back unchanged.max length 128
Request body application/jsonrequired
quoteId, alone — or sellAmount and buyCurrency (with optional accounts) to exchange at the current rate.merchantReferencestringrequiredYour own unique reference for this exchange; reusing it under a newIdempotency-Keyis400 duplicate_merchant_reference.min length 1 · max length 100
quoteIdstringmax length 64
sellAmountMoneyDecimal amount as a string — never a float, never minor units. An amount you send may carry no more decimal places than its currency has (and never more than 8); trailing zeros do not count, so10.100 EURis fine and10.123 EURis400 validation_failedonamount.value.valuestringrequiredExample:
149.90pattern ^-?\d{1,18}(\.\d{1,18})?$
currencystringrequiredISO 4217 code for fiat (EUR), asset ticker for crypto (USDT).Example:
EURpattern ^[A-Z0-9]{3,10}$
buyCurrencystringISO 4217 code for fiat (EUR), asset ticker for crypto (USDT).Example:
EURpattern ^[A-Z0-9]{3,10}$
sellAccountIdstringOpaque account identifier. Treat as a string; never parse or construct it.Example:
acc_AAAAAAAAAAcpattern ^acc_[A-Za-z0-9_-]{11}$
buyAccountIdstringOpaque account identifier. Treat as a string; never parse or construct it.Example:
acc_AAAAAAAAAAcpattern ^acc_[A-Za-z0-9_-]{11}$
Responses
Exchange accepted. The code is always 202 — read status: completed, failed or cancelled is
final; pending / processing means it is still settling (crypto pairs), so poll Location — Retry-After
is sent only then. An idempotent replay returns this original body.
Headers
Locationstring (uri-reference)The resource URL to poll for the current state (RFC 9110 §15.3.3 status monitor).Retry-AfterintegerSeconds to wait before pollingLocation. On a202it is present only while the body'sstatusis not yet terminal — its absence is a second signal that the work is already done.X-Request-IdstringThe client'sX-Request-Idif supplied, otherwise a server-generated id. EqualstraceIdin problems.Idempotent-Replayedbooleantruewhen this response was replayed from an earlier request with the sameIdempotency-Key.RateLimit-LimitintegerRateLimit-RemainingintegerRateLimit-ResetintegerSeconds until the window resets.
Schema application/json
objectstringrequiredAllowed values:
fx_exchangeidstringrequiredAn exchange's id. Opaque — use it as you received it.Example:
fx_019985c24a6e7c3b9f102d5e8a1b3c4dpattern ^fx_[0-9a-f]{32}$
merchantReferencestringAbsent on an exchange made in the portal.statusstringrequiredAllowed values:
pendingprocessingcompletedfailedcancelledsellMoneyrequiredDecimal amount as a string — never a float, never minor units. An amount you send may carry no more decimal places than its currency has (and never more than 8); trailing zeros do not count, so10.100 EURis fine and10.123 EURis400 validation_failedonamount.value.valuestringrequiredExample:
149.90pattern ^-?\d{1,18}(\.\d{1,18})?$
currencystringrequiredISO 4217 code for fiat (EUR), asset ticker for crypto (USDT).Example:
EURpattern ^[A-Z0-9]{3,10}$
buyobjectThe quoted amount until a venue exchange settles; then what it filled.valuestringrequiredExample:
149.90pattern ^-?\d{1,18}(\.\d{1,18})?$
currencystringrequiredISO 4217 code for fiat (EUR), asset ticker for crypto (USDT).Example:
EURpattern ^[A-Z0-9]{3,10}$
ratestringThe rate applied, as a string.feeobjectAbsent while it is not known — as on a portal exchange that has not executed yet, and on some older portal exchanges.valuestringrequiredExample:
149.90pattern ^-?\d{1,18}(\.\d{1,18})?$
currencystringrequiredISO 4217 code for fiat (EUR), asset ticker for crypto (USDT).Example:
EURpattern ^[A-Z0-9]{3,10}$
totalDebitedobjectPresent withfee.valuestringrequiredExample:
149.90pattern ^-?\d{1,18}(\.\d{1,18})?$
currencystringrequiredISO 4217 code for fiat (EUR), asset ticker for crypto (USDT).Example:
EURpattern ^[A-Z0-9]{3,10}$
sellAccountIdstringrequiredOpaque account identifier. Treat as a string; never parse or construct it.Example:
acc_AAAAAAAAAAcpattern ^acc_[A-Za-z0-9_-]{11}$
buyAccountIdstringOpaque account identifier. Treat as a string; never parse or construct it.Example:
acc_AAAAAAAAAAcpattern ^acc_[A-Za-z0-9_-]{11}$
createdAtstring (date-time)requiredRFC 3339, UTC, with millisecond precision.Example:
2026-09-18T10:15:30.123ZupdatedAtstring (date-time)requiredRFC 3339, UTC, with millisecond precision.Example:
2026-09-18T10:15:30.123ZcompletedAtstring (date-time)RFC 3339, UTC, with millisecond precision.Example:
2026-09-18T10:15:30.123Z