Skip to main content
POST
Executes a conversion. This moves real balance. The 201 response returns the conversion record — read its status.
201 ≠ finished. Which execution mode a conversion uses is a platform setting, resolved server-side — see the execution modes. In internal mode the 201 body already carries status: "completed" (the debit, credit and completion commit atomically). In external mode the full source amount is debited immediately and the 201 returns status: "processing" — no credit exists yet. You must poll GET /api/v1/conversions/:conversionId until status is completed or failed. Always drive your integration off the conversion status, never off the HTTP code.

Authorization

  • API key: the elevated conversions:execute scope. Possession of a key bearing conversions:execute is the credential — no MFA fields are required.
  • Dashboard JWT: the conversions:manage permission plus per-call MFA (otpCode, or passkeyResponse + challengeKey). MFA applies to dashboard (JWT) callers only. A JWT account with neither TOTP nor a passkey is refused (403 MFA_SETUP_REQUIRED).
Dashboard sessions must belong to a merchant_admin, merchant_member or super_admin role; API-key callers are gated by scope only. See the Authentication guide. Rate limit: 5/min.

Body

The body schema sets additionalProperties: false, but unknown fields are silently ignored (stripped before validation), not rejected — the request still succeeds. Double-check field names, especially idempotencyKey, quotedRate and quotedToAmount: a misspelled name is dropped without error, silently disabling the drift or idempotency protection it was meant to arm.
string
required
Source currency ticker.
string
required
Target currency ticker. Must differ from fromCurrency — same currency yields 400 SAME_CURRENCY.
string | number
required
Prefer the decimal string from the quote’s fromAmountStr. Numbers are accepted for backward compatibility.
string
A caller-chosen token that makes retries safe. Max 256 characters. Opt-in. Omitted → the server generates a unique key, so two identical requests execute twice. Double-submit protection is the caller’s job.
number
Must be greater than 0. The exchangeRate you showed the user. Triggers the drift guard.
string
The toAmountStr the user accepted. Binds the output, catching a fee move the rate check cannot see.
string
JWT callers: TOTP or a backup code.
object
JWT callers: passkey assertion instead of otpCode. Supply together with challengeKey.
string
JWT callers: accompanies passkeyResponse.
Always send your own idempotencyKey. Without one the server generates a unique key per request, so a network retry of the same submit executes a second conversion. With one, a retry of an already-completed conversion normally replays the original record as a 201 carrying the same _id — nothing executes twice. Any other collision is a 409 that tells you whether funds already moved — see idempotency conflicts. So treat a 201 on a retry as “already done”, not as a second execution: compare the _id.One caveat: the drift guard runs before the replay. A retry that echoes quotedRate/quotedToAmount during a >1 % market move returns 409 RATE_DRIFT first — even though the original request on that key already moved funds. A 409 RATE_DRIFT on a retry therefore does not mean nothing happened on the order. On any retry of a possibly-executed key, resubmit with the same key (never a fresh one) or reconcile via List conversions before treating the order as unexecuted.

Conversion record fields

Returned by POST / (201), GET / and GET /:conversionId.
toAmount, exchangeRate, commissionRate, commissionAmount and netFromAmount are all OPTIONAL and are ABSENT on any conversion that ended before a quote ever existed: a rate-drift or accepted-output-shortfall cancel, an execution-rate refusal, a failure released before pricing, or an interrupted initializing conversion recovered automatically. A conversion can legitimately reach a terminal state with none of them. Render — (or merchantMessage) when a field is missing — never substitute 0, which would read as a free conversion or a zero rate.
string
Conversion id, as a string.
string
Merchant id, as a string.
string
Source currency.
string
Target currency.
number
JSON number (display precision). Always present.
number
JSON number (display precision). Optional — see the warning above.
number
JSON number (display precision). Optional — see the warning above.
number
JSON number (display precision). Optional — see the warning above.
number
JSON number (display precision). Optional — see the warning above.
number
JSON number (display precision). Optional — see the warning above.
string
One of initializing, pending, processing, completed, failed, cancelled — see the status lifecycle.
string
"internal" or "external"; absent on older records (treated as internal).
string
External mode: the exact amount credited as a decimal string — normally at least the quoted minimum, but a late-deposit shortfall fill can be lower. Use this, not toAmount or the quote, for reconciliation.
number
External mode: the price-protection band of the accepted quote.
string
Merchant-safe explanation of a terminal outcome. The only failure text you should ever render.
string
Echoed when you supplied one.
string
ISO string.
string
ISO string.
string
ISO string.
string
ISO string.
Never returned (stripped at the API boundary): the internal error text, the provider request id, the provider deposit address, the exact funding amounts and both on-chain transaction hashes. Do not build against them.

Rate and output drift

409 RATE_DRIFT. Tolerance is 1 %. Two independent guards protect you:
  • Rate drift — if you send quotedRate and the market moved more than 1 %, the request is refused before your MFA factor is consumed:
    409 RATE_DRIFT
    Re-present newQuote to the user and resubmit. A passkey assertion is single-use — obtain a fresh one before retrying. A TOTP code stays valid inside its window. In internal mode the server also re-checks the rate against an uncached spot rate at execution time, so the guard holds even if you omit quotedRate. In external mode there is no such re-check — the only guards are the quotedRate and quotedToAmount you send — so on an external pair, omitting them means the conversion executes at whatever the provider quotes with no drift veto at all. Always send them.
  • Output shortfall — if you send quotedToAmount, a freshly computed output more than 1 % below it also raises 409 RATE_DRIFT. This is the only guard that catches a fee change between quote and execute (the rate alone cannot see it). It is one-sided on purpose: better-than-accepted terms always execute.
No funds moved by this request. (If this key’s original request already executed, that conversion still stands — see the idempotency tip above.) The rate-drift and output-shortfall guards inside the service mark the row cancelled; a request rejected earlier — at validation, or by the idempotency gate — never creates a row to cancel.

Errors

The status code is the reliable signal, but read the code with it. For the two idempotency codes (CONVERSION_IN_PROGRESS, IDEMPOTENCY_KEY_CONSUMED), 409 = your key collided, nothing new was executed by this request — funds may already have moved on the original request, so reconcile rather than re-quote. A 409 RATE_DRIFT is separate: the market moved, so re-present newQuote. A 400 CONVERSION_FAILED is a genuine domain failure.

See also

  • Get a conversion: the status source of truth — poll it after any external-mode 201.
  • Get a quote: where fromAmountStr, quotedRate and quotedToAmount come from.
  • Get MFA options: which factor a dashboard user can present on this call.
  • Conversions Overview: execution modes, status lifecycle and the idempotency-conflict contract.