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
| Action | Safe UI rule |
|---|---|
| Delete | Removes the order from that session's history only. It does not cancel a workflow or reverse money. |
| Coinflip | Show 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. |
| Review | Available once, after completion. Send all three integer ratings from 1 to 5; the optional comment is at most 500 characters. |
| Refund address | Show 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.
code | Typical status | What to do |
|---|---|---|
invalid_request | 400 | Terminal. Fix the request—but see deliberately generic refusals before assuming the request is at fault. |
unauthorized | 401 | Terminal. The credential is missing, malformed, or sent in the wrong place. |
forbidden | 403 | Terminal. The credential is valid but not entitled to this route. |
not_found | 404 | Terminal. On protected order reads this is deliberately also the answer for unauthorized. |
conflict | 409 | Re-read the order, then present the next action from its current state. Do not resubmit blindly. |
amount_below_minimum | 400 | Terminal until the amount changes. Read minAmountUsd and re-quote. |
no_route | 400 | No route is available for that pair and amount right now. Retryable later or at a different amount; never in a tight loop. |
rate_limited | 429 | Wait for retryAfter and the Retry-After header. |
unavailable | 503, 500, 504 | Temporary and ours, not yours. Retry with backoff. |
request_failed | 400 | An upstream leg failed and was neither a routing nor a rate-limit answer. Re-read the order before retrying. |
429: wait forretryAfterseconds and theRetry-Afterheader before retrying.503withunavailable: 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 from400: 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.