Asynchronous Writes
Understand why trading writes answer before the exchange decides their outcome, how to read the outcome, and how to retry safely.
Every operation documented as an asynchronous write in the REST API reference or WebSocket API reference acknowledges submission before the exchange decides its outcome. Account and grant setup can also continue after the response. The references are the source of truth for which operations exist; this page explains how to reconcile any of them.
The acknowledgment therefore confirms acceptance, not the result. Read the result from the resource itself.
Read the Acknowledgment
An accepted write answers 202 Accepted. The body carries the key you read the outcome with, and each key has one place to read it:
| Write | Response key and where to read the outcome |
|---|---|
| Place an order | order_id; read Get Order. |
| Amend or cancel an order | order_id; read Get Order. |
| Cancel orders in bulk | No key; poll List Orders with status=resting&status=partially_filled and the filters you submitted until it returns no orders. |
| Create an RFQ | Your client_rfq_id; read List Account RFQs with your account_id and filtered by that client identifier. |
| Create a quote | Your client_quote_id; read List Quotes with your account_id and that client identifier. |
| Cancel or confirm a quote | quote_id; read Get Quote. |
| Accept a quote | order_id and quote_id; read Get Quote for the last-look outcome, then Get Order by the returned order_id to reconcile the acceptance. |
| Create an account | The pending account, or your client_account_id; read Get an Account by Client Account ID. |
| Grant account access | The pending grant; read List Grants. |
The exchange assigns rfq_id and quote_id only once the RFQ or quote lands, which is why creation echoes your client ID instead.
Every write command answers a *_submitted frame echoing the command’s id and the key you read the outcome with: the order_id for the order commands (placing also echoes your client_order_id), the quote_id plus the created order’s order_id for an accept, the quote_id plus your client_quote_id for a confirmation or cancellation, and your client_rfq_id or client_quote_id for create_rfq and create_quote — the exchange assigns rfq_id and quote_id only once the RFQ or quote lands, so their creation echoes your client ID instead. The stream that carries the committed outcome is independent of the command reply, so a stream frame may arrive before the *_submitted acknowledgment; correlate by the echoed keys, never by frame order.
For create_rfq and create_quote, subscribe to the rfqs or quotes channel before you send, and treat the rfq_opened or quote_opened frame carrying your client_rfq_id or client_quote_id as the committed outcome.
A rejected command answers an error frame that echoes the command’s id and leaves the connection open; the command’s reference page lists the codes it can send.
Read the Outcome
Poll the resource with bounded backoff until it shows the state you expect.
For account setup, poll List Accounts until the account is active. A failed status is not terminal; setup is retried.
Absence and unchanged state are inconclusive. A newly accepted write can take time to become visible, and the exchange can reject a write after accepting it. A rejected order appears with the status rejected and a rejection_reason.
The exchange defines no deadline after which absence proves that a write failed. Poll until your own workflow’s deadline, then treat the outcome as unresolved. Never submit a replacement under a new client ID because the first write is not yet visible.
The orders subscription carries an order’s outcome and the fills subscription its executions. The rfqs subscription carries an RFQ’s outcome. The quotes subscription carries a quote’s.
Absence is inconclusive. If no frame arrives by your own workflow’s deadline, list your orders, your account’s RFQs, or your quotes over REST filtered by your client ID, then treat the outcome as unresolved. Never submit a replacement under a new client ID because the first write is not yet visible.
Working State Versus History
Reads of working orders are served from the live working view. This covers Get Order and List Orders filtered to active statuses only: status=resting, status=partially_filled, or both.
Every other list, with no status filter or with any terminal status (filled, canceled, expired, rejected), is served from exchange history. Exchange history converges later than the live working view and is not a snapshot across pages. Use history for terminal accounting, not for completion checks.
After a bulk cancellation, each read proves one thing:
- A
202proves the exchange accepted the cancellation for processing. - An empty active-status list proves that every order the exchange has processed is no longer working.
- A history row still reading
restingonce the active-status list is empty is history catching up, not an order still on the book. Read that order by its ID to see its current state from the live working view.
To confirm each cancellation as it commits, subscribe to the WebSocket orders channel before you cancel; it delivers one lifecycle frame per state change, order_canceled among them.
Outcome Unknown
A 503 Service Unavailable means the request could not be completed synchronously. For a write, use the problem’s type to tell these cases apart:
- The write was never submitted. The problem has
type: urn:udx:problem:request-admission-unavailable. Retry later; nothing was submitted. - The outcome is unknown. The problem’s
typenames the write and ends inoutcome-unknown, for exampleurn:udx:problem:order-outcome-unknown. The response carriesRetry-After. Wait at least that long, then read the outcome as described above before deciding whether to retry.
For a public history read, including account RFQ or quote lists, the problem type urn:udx:problem:could-not-process-retry means the read exceeded UDX’s upstream deadline. The response carries Retry-After and Cache-Control: no-store; retry the same read after the advised interval. It does not mean that a write was submitted or that the resource changed.
Do not use detail to choose a recovery path.
An error frame with the code admission_unavailable means either the command was never submitted or the submission’s outcome could not be confirmed; the code does not distinguish the two. An internal_error frame, or a connection that closes before any frame answers the command, leaves the outcome unknown the same way. In every case treat the write as unresolved: reconnect if you must, then read the order, RFQ, or quote over REST or in the snapshot of a new subscription before deciding whether to retry.
Do not use detail to choose a recovery path.
Retry Safely
A retry is the identical request: the same method, path, query string, body, client ID, and idempotency key, if one was sent. With the same UDX-Idempotency-Key, the exchange returns the original response instead of executing again. Each attempt needs a new timestamp and signature.
The exchange rejects a second order that reuses a client_order_id within the same account and market, so an accidental duplicate becomes a rejected order rather than a second position. The second request is still accepted with 202; its order_id reads back as rejected. The same ID on another market is a new order. RFQ and quote client IDs carry no such protection: a repeated request creates a second RFQ or quote. The exchange rejects a second account that reuses a client_account_id within your member with 409 Conflict; read the account by that ID to find the one the first request created. Use idempotency keys for replay-safe retries of any write.
A retry is a new command with a fresh id and the same params, including the same client ID. The session was signed once at connect, so nothing else changes.
Nothing on the WebSocket deduplicates the retry: unlike REST it has no idempotency-key guard, and idempotency_key is carried but not yet deduplicated on. The exchange still rejects a second order that reuses a client_order_id within the same account and market, so a duplicated create_order becomes a rejected order rather than a second position. RFQ and quote client IDs carry no such protection: a resend can create a second RFQ or quote. Retry only once you have confirmed — your orders by client_order_id, or the RFQ or quote over REST — that the first write does not exist.