Handle outcomes & timeouts
What to do with each /trade outcome — when to resend, when to reconcile, and how to never double-fill.
POST /trade is synchronous and returns the same JSON shape whatever the HTTP status, so branch on submission_status in the body, not the status line. (The one exception is a body the service rejects before it reaches the handler — over the 256 KiB limit — which comes back as plain text with no submission_status at all. Guard your parse.) This guide is the decision playbook; the full code catalog is in Error responses.
Read two fields, not one
submission_status tells you whether the transaction landed. It does not tell you whether the order succeeded — that lives in the response envelope, present on every accepted reply.
submission_status
What happened
What to do
accepted
The transaction landed and reached execution.
Not done — read response. It carries {"open":…}, {"filled":…}, {"cancelled":…}, or {"error":"<code>"}.
rejected
Refused before execution (shaping / rate limit / suspension / expiry / admission), or an envelope-level execution failure. error.code says why.
Fix the cause, submit a fresh action. One exception below.
timeout
The outcome was not observed in the 3-second budget, or the submission could not be routed.
Depends on the code — see below.
accepted is not success. An order that failed at execution — insufficient balance, below minimum notional, off the tick grid — still returns accepted with no top-level error; the code appears only inside response, at response.status.error for a single order. A client that branches on submission_status alone records a rejected order as live and will keep quoting against a position it never had.
{
"submission_status": "accepted",
"tx_hash": "0x...",
"response": { "type": "order", "status": { "error": "insufficientspotbalance" } }
}The full leaf vocabulary is in POST /trade. You only need orderStatus afterwards to reconcile a timeout, or to re-read an order later in its life.
The one safe resend
RateLimited (HTTP 429) is the only rejection you resend as-is: back off error.retry_after_ms, then send the same signed action. Every other rejected needs a fresh action after you fix the cause — never blindly resend.
Reconciling a timeout
Not every timeout is indeterminate. The error.code tells you whether the transaction ever reached a node, and the two cases need opposite handling.
(none) — the wait budget elapsed
200
Yes, it is executing
Reconcile by cloid. Never resubmit under a new nonce.
HandoffBufferFull:{request_count|bytes|signer}
503
No — refused before any submission was attempted
Resubmit. There is nothing to reconcile.
HandoffTimeout / HandoffMultipleActive
503
No — no writable node accepted it
Resubmit, or reconcile first if a duplicate would be costly (see below).
node_unreachable: …
504
Unknown — the connection broke mid-submission
Reconcile by cloid. Never resubmit under a new nonce.
Treating the whole 503 family as indeterminate silently drops every write for the duration of a leadership handoff, which is why it is worth separating. Treating the 200 and 504 cases as safe to resubmit is how you double-fill.
HandoffBufferFull is refused before any node is contacted, so a resubmit cannot duplicate. HandoffTimeout and HandoffMultipleActive mean every attempt either failed to connect or was explicitly refused — so a resubmit is expected to be safe, but the guarantee rests on the service classifying the connection failure correctly. If a duplicate fill would be expensive for you, reconcile by cloid first and resubmit only when the lookup comes back empty; you still recover the order, just one round trip later.
When a code is not in this table, reconcile.
To reconcile, look the action up by the cloid you sent:
orderStatus— an order's current lifecycle bycloid.txStatusByCloid— a non-order action (withdraw/settle/repay) bycloid, within the recent window.
This is why every order should carry a cloid — it is your only handle for reconciliation.
Batches
A batch is one envelope with one submission_status, but its response.statuses[] carries one sub-response per item, in item order — so an accepted batch already tells you which items rested, filled, or failed. Read each item's outcome at statuses[i].status, one level below where a cancelAll leaf sits. Reconcile per item by cloid via orderStatus only when the envelope came back timeout.
Next steps
POST /trade — the full
responseenvelopeError responses — every code and what it means
Trade over REST — the happy path
Last updated