Skip to main content

Actions and retries

All owner actions require the x-session-id returned when the order was created. A destination verify value can read one order only; it cannot mutate it.

Actions

ActionSafe UI rule
DeleteRemoves the order from that session's history only. It does not cancel a workflow or reverse money.
CoinflipShow only while customerState is { kind: "action_required", action: "coinflip" }. Read the game details from coinflip. Send an explicit accept: true or accept: false decision; an empty body is rejected. A 409 means refresh; a 504 may mean the decision was accepted, so refresh before retrying.
ReviewAvailable once, after completion. Send all three integer ratings from 1 to 5; the optional comment is at most 500 characters.
Refund addressShow only while customerState is { kind: "action_required", action: "refund_address" }. It saves one immutable customer destination; it never starts a transfer. A structured refundRecovery.kind: "wrong_chain" exposes the server-derived receipt while its declared operator recovery is pending or running.

customerState is the action authority. Supporting resources provide data but do not independently make an action available; never infer an action from status, coinflip, refundRecovery, or prose error text.

Errors

Every public failure uses one closed envelope:

{ "code": "amount_below_minimum", "error": "Amount is below the minimum.", "minAmountUsd": 20 }

code is the stable field to branch on. error is sanitized prose for a human and may change at any time—never branch on English text. retryAfter (seconds) is present when waiting is the correct response, and minAmountUsd only on amount_below_minimum.

codeTypical statusWhat to do
invalid_request400Terminal. Fix the request—but see deliberately generic refusals before assuming the request is at fault.
unauthorized401Terminal. The credential is missing, malformed, or sent in the wrong place.
forbidden403Terminal. The credential is valid but not entitled to this route.
not_found404Terminal. On protected order reads this is deliberately also the answer for unauthorized.
conflict409Re-read the order, then present the next action from its current state. Do not resubmit blindly.
amount_below_minimum400Terminal until the amount changes. Read minAmountUsd and re-quote.
no_route400No route is available for that pair and amount right now. Retryable later or at a different amount; never in a tight loop.
rate_limited429Wait for retryAfter and the Retry-After header.
unavailable503, 500, 504Temporary and ours, not yours. Retry with backoff.
request_failed400An upstream leg failed and was neither a routing nor a rate-limit answer. Re-read the order before retrying.
  • 429: wait for retryAfter seconds and the Retry-After header before retrying.
  • 503 with unavailable: a temporary refusal—a paused system, a paused withdrawal, an asset or chain currently switched off, or a dependency of ours that never answered. It is distinct from 400: nothing in the request needs changing, so retry later rather than re-deriving the payload.
  • Protected order 404: deliberately means either nonexistent or unauthorized/missing proof. Never use it as an existence check.
  • 409: re-read the order before presenting another action.
  • 5xx: the result can be uncertain; read durable state before submitting another owner action.

Order creation is metered twice: the general per-IP request limit, and a separate cap on how many unfinished orders one IP may hold at once. Either answers 429 with Retry-After; the second can fire while ordinary reads and quotes still succeed.

The second one is a quota, not a rate. It counts orders that are still open — awaiting a deposit, or still processing — and never orders that completed, failed, expired or were refunded, because what it bounds is the cost an abandoned order leaves running. So a caller that finishes its orders is never throttled by how many it has created, and the remedy in the message is literal: completing one frees a slot immediately, and an abandoned one frees itself when it expires.

Deliberately generic refusals

A bare 400 { "code": "invalid_request", "error": "Invalid request." } covers two different things: a rejected request body and a request refused by policy. They are intentionally indistinguishable, for the same reason a protected order read returns 404 for both missing and unauthorized: a distinguishable refusal turns order creation into a probe.

Field-level validation is the exception that proves it—when the API can name the offending field it does, in error (missing/invalid field: toAddress). The flat Invalid request. copy is the one that tells you nothing on purpose.

Do not build a retry loop, a discovery probe, or an address-validity check on this response. Treat it as terminal for that request and surface a plain "this request could not be created" to the user.

Retry policy

GET reads and quotes may be retried with bounded backoff. After a WebSocket reconnect, perform an authorized REST read.

Do not automatically retry creation after a timeout. The current create routes do not offer idempotency, so another POST can create another order. A caller-supplied valid sessionId, authenticated by an integrator API key, groups ownership and can help recover known session orders; it is not an idempotency key or deduplication mechanism.