Order Sessions and Recovery
Bind order delivery to a session instead of a connection, so an order’s outcome survives a dropped connection and can be recovered
Session-Bound Order Delivery
Section titled “Session-Bound Order Delivery”An order can outlive the connection that placed it. The connection can drop between the moment we accept your order and the moment we know its outcome, and no amount of retrying on your side changes what has already been sent to the market.
This page describes the model that makes that outcome recoverable: sessions, order subscriptions, and the three ways to recover an outcome you did not receive.
The Two Delivery Models
Section titled “The Two Delivery Models”| Legacy delivery (default) | Session-bound delivery | |
|---|---|---|
| How you select it | Send the order without autoSubscribe, or with autoSubscribe: false |
Send the order with autoSubscribe: true |
| Where the outcome goes | The connection that submitted the order | The session, whichever connection currently holds it |
| Frames on the order exchange | One terminal order response |
PENDING first; the terminal arrives separately |
| If the connection drops first | The outcome is lost | The outcome is stored and redelivered when you reconnect |
| Redelivery on reconnect | Never | Until you send orderUnsubscribe, up to 24 hours |
| Recovery available to you | Read Order State, Get Trades, idempotency replay | All of those, plus redelivery |
Sessions
Section titled “Sessions”What a Session Is
Section titled “What a Session Is”A session is a named delivery destination that outlives any one connection. Its identity is the sessionId you supply on the WebSocket handshake:
wss://trade-uk.zodiamarkets.com/zm/ws/ws-client?token={token}&sessionId={sessionId}The session scopes where an order outcome is delivered and what is redelivered when you reconnect. It does not scope access: your orders are visible only to your own credentials, whatever sessionId is presented, and no sessionId gives access to another counterparty’s orders.
Who Chooses It
Section titled “Who Chooses It”You do. Choose a stable identifier — one per logical client process — and reuse it across reconnects. That reuse is the whole mechanism: reconnecting under the same sessionId is what rejoins the session and collects what you missed.
If you omit the parameter, or send it empty, the gateway mints a fresh identifier for that connection alone. Nothing is then recoverable across a reconnect, because the next connection is a different session.
Format
Section titled “Format”- 1 to 64 characters.
- Must not contain
/, whitespace or control characters. Dots and colons are allowed. - The value is percent-decoded before it is validated, so
a%2Ebanda.bare the same session.
A value that breaks these rules refuses the connection. You receive one error frame and the socket closes:
{ "messageType": "error", "message": "sessionId must be 1..64 characters and must not contain '/', whitespace or control characters", "code": "", "timestamp": 1718113605032}The refusal carries no error code, and the same message is returned for every malformed value — including one that is not valid percent-encoding. sessionId is checked before your token is, so a refused connection does not consume the one-shot authentication token.
Reconnecting Under the Same Session
Section titled “Reconnecting Under the Same Session”Connecting with a sessionId that is already live for your account closes the earlier connection. The new connection wins; the older one is dropped without an error frame and without an application close code — from its side the transport simply goes down.
This is deliberate: a session has one holder, so an outcome is never delivered to a socket you have abandoned. Two practical consequences:
- Do not open two concurrent connections under one
sessionId. If you want two concurrent sockets, give them two differentsessionIdvalues — they are then two sessions, each recovering only its own orders. - A reconnect loop must not race itself. If your client can have two reconnect attempts in flight, the second will close the first.
A different counterparty presenting the same sessionId closes nothing of yours.
How Long the Session Is Remembered
Section titled “How Long the Session Is Remembered”Two different clocks, and they answer two different questions.
| What it covers | How long | |
|---|---|---|
| Live awaits | Orders still in flight, whose terminal has not arrived yet | The session’s delivery scope survives 1 hour of observed absence. It is swept once a minute, so the worst case is 1 hour and 2 minutes. |
| Stored outcomes | Orders whose terminal has been recorded | 24 hours, refreshed on each write to the record |
Reconnect within the first window and your in-flight orders are still bound to the session, so their terminals are pushed to you when they land. Reconnect after it and the in-flight awaits are gone — but a terminal that was recorded is still redelivered, and still readable over REST, for 24 hours.
If You Do Not Use autoSubscribe
Section titled “If You Do Not Use autoSubscribe”Omit sessionId. It buys an unsubscribed client nothing — no order is placed in a redelivery worklist unless it was sent with autoSubscribe: true — and it exposes you to two failure modes that would otherwise not exist: a refused connection if the value is malformed, and self-eviction if your reconnect logic reuses it while an earlier socket is still live.
Subscribing to an Order
Section titled “Subscribing to an Order”There is no subscribe message. You subscribe to an order’s outcome by setting autoSubscribe: true on the order request itself:
{ "messageType": "order", "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...", "side": "BUY", "clientRequestId": "TESTCLIENT1", "accountGrpUuid": "7687282f-1073-441b-9ff4-694e6b49effe", "quantity": "100000", "autoSubscribe": true}The subscription is created as part of accepting the order, and it is fail-closed: if we cannot record it, the order is refused rather than accepted, with RFS100012 and the message Failed to register order subscription. We do not promise delivery we cannot track.
The PENDING Response
Section titled “The PENDING Response”The reply on the order exchange is an order message with orderStatus: "PENDING". It means accepted, outcome not yet known. It is not a terminal state, and the economics it carries are those sealed into the quote the order was accepted against — not a confirmation that anything was dealt at them.
{ "messageType": "order", "orderStatus": "PENDING", "subscriptionStatus": "SUBSCRIBED", "sessionId": "trading-desk-1", "clientRequestId": "TESTCLIENT1", "transactionId": "774722250a694498b916b7bac07bb48e", "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...", "instrument": "USDC.AED", "side": "BUY", "price": "3.673050", "quantity": "100000", "tenor": "T", "settleDate": "20240811", "message": "", "code": "", "timestamp": 1718113605032}subscriptionStatus and sessionId appear only on responses to subscribed orders. An order sent without autoSubscribe gets neither field on the wire at all.
Record the transactionId. It is the only identifier that ties the PENDING, the terminal, every redelivery of that terminal, and your orderUnsubscribe together.
The Terminal
Section titled “The Terminal”The terminal order response — SUCCESS, FAILED or INDETERMINATE — is delivered to the session, not as a reply on the order exchange. Whichever connection holds the session at that moment receives it.
That has one consequence a client architect must design for:
Delivery is at-least-once. The same terminal can arrive more than once — on a reconnect, or after a push whose holder had already gone. Deduplicate on transactionId and treat a repeat as a no-op.
Do not rely on arrival order either. Treat PENDING as ignorable once you have seen a terminal for that transactionId, and never make a state transition conditional on PENDING having arrived first.
If the Terminal Never Arrives
Section titled “If the Terminal Never Arrives”A subscribed order is held for up to 1 hour. If nothing terminal arrives in that time, nothing is emitted — the order stays PENDING and is recovered through redelivery or Read Order State. This differs from legacy delivery, where the gateway fabricates an error terminal (RFS100012, Failed to send order to broker) after 30 seconds so the exchange has a reply.
Per-Session Capacity
Section titled “Per-Session Capacity”A session may hold 500 orders awaiting a terminal at once. Beyond that, a new subscribed order is refused before anything is placed:
{ "messageType": "error", "code": "RFS100027", "message": "Per-session order capacity reached (not a throughput limit): too many orders in flight on this session, retry once earlier orders resolve", "clientRequestId": "TESTCLIENT1", "transactionId": "0f2c1de37e5b41a0a3b8b2c1de37e5b4"}This is not the rate limiter. RFS100026 means you are sending too fast and should back off and re-quote; RFS100027 means too many of your orders are unresolved and you must let them resolve — which can take as long as the one-hour await ceiling. If you meet it routinely, you are most likely not unsubscribing.
Acknowledging with orderUnsubscribe
Section titled “Acknowledging with orderUnsubscribe”orderUnsubscribe is your acknowledgement of receipt, and the only one the protocol has. Until it arrives, we assume you have not got the outcome, and we redeliver.
An order you never unsubscribe from is redelivered on every subsequent connection under that session, for up to 24 hours. That is by design — it is what makes at-least-once delivery safe — but it means an integration that never acknowledges accumulates redelivery traffic and eventually meets the capacity refusal above.
Send it once you have durably recorded the terminal.
Unsubscribe Request
Section titled “Unsubscribe Request”| Field | Type | Required | Description |
|---|---|---|---|
messageType |
string | yes | Must be orderUnsubscribe. |
transactionId |
string | yes | The transactionId of the order you are acknowledging, taken from its order response. Only an order whose terminal has already been delivered may be acknowledged — sending this while the order is still in flight is refused, because it would cut you off from the outcome. Orders are tracked for 24 hours; after that the reference is no longer known. |
{ "messageType": "orderUnsubscribe", "transactionId": "774722250a694498b916b7bac07bb48e"}Unsubscribe Response
Section titled “Unsubscribe Response”| Field | Type | Required | Description |
|---|---|---|---|
messageType |
string | yes | Always orderUnsubscribe. Unlike unsubscribe, this message is sent for every request, refusals included — read success. |
timestamp |
integer (int64) | yes | Unix epoch milliseconds, UTC — when this message was built. |
success |
boolean | yes | true when the order is now acknowledged and will not be redelivered — including when it already was, so a repeat is a success rather than an error. false means nothing changed: the order is still in flight, unknown to us, or not yours. No further order message arrives for an acknowledged order. |
message |
string | yes | Which of the five outcomes this was. Unsubscribed — done. Order already unsubscribed — a repeat, also a success. Order is still in flight; unsubscribe is only allowed once a terminal result is delivered — wait for the terminal and retry. Unknown order for transactionId=… — no such order, or it is not yours, or it has aged out of the 24-hour tracking window; the three are deliberately indistinguishable so that probing cannot confirm another counterparty’s order exists. Failed to persist unsubscribe for transactionId=… — a transient failure our side, safe to retry. |
transactionId |
string | yes | The transactionId from your request, echoed. |
code |
string | yes | Empty on success — including on a repeat acknowledgement, which is a success. RFS100012 on a refusal; message says which of the three refusals it was. |
Every outcome, in the order they are evaluated:
| Condition | success |
code |
message |
|---|---|---|---|
| No such order, or not yours | false |
RFS100012 |
Unknown order for transactionId=<id> |
| Already unsubscribed | true |
"" |
Order already unsubscribed |
| Terminal not yet delivered | false |
RFS100012 |
Order is still in flight; unsubscribe is only allowed once a terminal result is delivered |
| Could not be recorded | false |
RFS100012 |
Failed to persist unsubscribe for transactionId=<id> |
| Unsubscribed | true |
"" |
Unsubscribed |
Three properties worth designing around:
- It is idempotent. A repeat request succeeds. If your acknowledgement is lost, send it again — you cannot double-acknowledge.
- Mid-flight unsubscribe is refused. You cannot unsubscribe from an order whose outcome you have not received; that would orphan you from it. Wait for the terminal.
- An unknown order and someone else’s order are reported identically.
Unknown order for transactionId=…does not distinguish the two.
An orderUnsubscribe for an order placed without autoSubscribe succeeds once that order has a terminal. It is a no-op — such an order was never in a redelivery worklist — and it is not an error.
Reconnecting
Section titled “Reconnecting”Every new connection triggers a redelivery pass. A first connection has nothing to redeliver; a reconnect under an existing sessionId collects what the session is holding.
What is redelivered: the recorded terminal of every order in that session that is still subscribed and belongs to you.
What is not:
- Orders still in flight. They have no recorded terminal yet; their terminal is pushed when it lands.
- Orders you have unsubscribed from.
- Orders placed without
autoSubscribe. They are never in a redelivery worklist — read them over Read Order State instead. - Price subscriptions. Those are bound to the connection and always have been; re-subscribe after every reconnect.
A redelivered terminal is the same message, in the same shape, as the first delivery. You cannot tell them apart, and you should not need to — deduplicate on transactionId.
At most 500 orders are redelivered per reconnect, and you are not told when that limit is reached. The remainder arrives on your next reconnect, or can be read over REST at any time. An integration that unsubscribes promptly will never approach it.
Recovering a Lost Outcome
Section titled “Recovering a Lost Outcome”Three mechanisms, and they answer different questions. A client that has lost an outcome should be able to pick one from this table without reading further.
| Mechanism | Available to | What it gives you | Use it when |
|---|---|---|---|
| Redelivery on reconnect | Subscribed orders only | Every recorded terminal the session still holds, pushed automatically | Always — it is automatic. It is the primary path for a subscribed client. |
| Read Order State | Every client, both delivery models | The current state of a named order, or of all your orders, on demand | You need an answer now, you are not subscribed, or you are reconciling |
| Idempotency replay | Every client, both delivery models | The original order’s outcome, in place of executing a second order | The send itself may have failed and you do not know whether the order exists |
They compose, and the order matters:
- If you are unsure whether the order was ever placed, retry it with the same
idempotencyKey. This is the only mechanism that is safe when the failure was on the way in. It either places the order (it never arrived) or returns the original’s outcome (it did) — see Idempotency. Never retry with a fresh key: that places a second order. - If you know the order was accepted but you lost the outcome, reconnect under the same
sessionIdand let redelivery serve it — or, if you are not subscribed, read it. - If you need a definitive answer at a point in time — for reconciliation, or after the session’s windows have expired — read the order state. It covers both delivery models and is the only path that does not require a WebSocket connection at all.
For an order that has settled, Get Trades remains the record of the executed trade; clientRequestId becomes its clientRef.
Migration Checklist
Section titled “Migration Checklist”Moving an existing integration from legacy to session-bound delivery:
- Choose a
sessionIdper client process and pass it on the handshake. Make it stable across reconnects and unique across concurrent processes. - Set
autoSubscribe: trueon order requests. - Stop treating the order reply as the outcome. Expect
PENDINGon the exchange, and handle the terminal wherever it arrives. - Correlate on
transactionId, and deduplicate on it. Acceptorderresponses that no request on this connection asked for. - Send
orderUnsubscribeonce each terminal is durably recorded. - Handle
RFS100027by letting open orders resolve, not by backing off and re-sending. - Keep your
idempotencyKeydiscipline. It is unchanged by any of this, and it remains the only defence against a failed send.