Subscribe to Order Channel
Once prices are streaming, a quote can be executed using the order message
Order Execution
Section titled “Order Execution”Execute trades instantly on live prices from your price stream subscriptions →.
Overview
Section titled “Overview”After subscribing to a price stream, use the quoteId from price updates to execute trades. Orders are filled at the displayed price with immediate confirmation.
Order Delivery
Section titled “Order Delivery”This page describes the default delivery model: you send an order, and one terminal order response comes back on the connection that sent it. If that connection drops before the response arrives, the response is lost — while the order may still execute.
Setting autoSubscribe: true on the order request binds delivery to a session instead. The outcome is then stored, delivered to whichever connection holds the session, and redelivered when you reconnect. Everything else on this page — the request fields, idempotency, the status values, signed intent — applies unchanged to both.
Order Request
Section titled “Order Request”Execute an order using a quote ID from a price stream update →.
Request Fields
Section titled “Request Fields”| Field | Type | Required | Description |
|---|---|---|---|
messageType |
string | yes | Must be order. A value outside the protocol’s set is discarded silently, with no reply — on the order path that is indistinguishable from a message that never arrived, so validate before sending. |
quoteId |
string | yes | The quoteId from the pricestream update you are dealing on, copied unchanged. It carries the price, size, tenor and settlement date of that update, so those are taken from it and not from this request — an instrument, tenor or settleDate sent here is replaced. It expires ten minutes after issue (RFS100019) and is bound to your user (RFS100020). |
side |
string (enum) | yes | BUY (execute offer) or SELL (execute bid) — One of: BUY, SELL |
clientRequestId |
string | yes | Your unique identifier for this order — 1–64 characters with no control characters, or the order is refused with RFS100012. This is the key the order-state read looks orders up by, and it becomes clientRef on the trade. |
accountGrpUuid |
string | conditional | Account group UUID →. Please check with your Account Manager your Account Group setup |
quantity |
string | How much to deal, as a decimal string — not a JSON number. It is denominated in the currency named on the subscription behind this quote, not necessarily the base currency of the pair, and this message carries nothing that says which: the client that made the subscription is the one that knows. Omit it to deal the full quoted size. More than the quoted size is refused with RFS100009; less is dealt in full at the quoted price. Orders are all-or-nothing — there is no partial fill, and the quantity on the response is always the quantity you asked for. |
|
idempotencyKey |
string | Makes a retry of this order safe. A later order carrying the same key is not executed — the original order’s outcome is replayed instead, on the original’s transactionId. 1–64 characters, no colon, no whitespace and no control characters; dots are allowed. Scoped to your user and valid for 24 hours from first use. See Idempotency. |
|
paymentReason |
string | conditional | ISO payment-reason code. Required when executing on a third-party beneficiary or third-party sender price stream; see the code tables on 3rd Party Beneficiary Delivery Price Channel. |
autoSubscribe |
boolean | Opt in to session-bound delivery of this order’s outcome. false (the default) delivers one terminal order response on the connection that submitted the order, and nothing survives a disconnect. true returns PENDING immediately, delivers the terminal to the session rather than the connection, and redelivers it on reconnect until you send orderUnsubscribe — see Order Sessions and Recovery. — Default: false |
|
signedIntent |
object | Optional cryptographic proof of order intent — see Subscribe to Order Channel with Signed Payload. | |
signedIntent.payload |
string | Base64-encoded JSON containing order parameters | |
signedIntent.signature |
string | Base64-encoded RSA signature of the payload |
Understanding Side
Section titled “Understanding Side”When executing on currency pair USDC.AED:
| Side | You’re Executing | You Pay | You Receive |
|---|---|---|---|
BUY |
The offer (ask) | AED | USDC |
SELL |
The bid | USDC | AED |
Rule:
BUY= You buy the base currency (first in pair)SELL= You sell the base currency (first in pair)
Idempotency
Section titled “Idempotency”Recovering an Order After a Failed Send
Section titled “Recovering an Order After a Failed Send”Use the optional idempotencyKey field to make retrying an order safe. A retry carrying the same key never places a second order: Zodia Markets replays the outcome of the first one.
How it works:
- The first order carrying a given key is executed normally.
- A later order carrying the same key is not executed. You receive the original order’s outcome, correlated to the
clientRequestIdyou sent on the retry and carrying the original order’stransactionId. - If the original order is still in flight, the retry waits for it and returns its result.
- Keys are scoped to your user and expire 24 hours after first use.
Request Example with Idempotency Key
Section titled “Request Example with Idempotency Key”{ "messageType": "order", "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...", "side": "BUY", "clientRequestId": "TESTCLIENT1", "idempotencyKey": "ORDER-2026-04-20-001-USDC-AED", "accountGrpUuid": "7687282f-1073-441b-9ff4-694e6b49effe", "quantity": "100000"}What a Retry Receives
Section titled “What a Retry Receives”| State of the original order | What the retry receives |
|---|---|
Already executed (SUCCESS, FAILED or INDETERMINATE) |
The original order’s order response, carrying the original’s transactionId and your retry’s clientRequestId. |
| Still in flight | The original order’s order response as soon as it lands, within 30 seconds. |
| Still in flight after 30 seconds | An error with code RFS100012 and the message Order is still processing; please retry. This is not a failure — retry again with the same key. |
No second order reaches the market in any of these cases.
An order originally submitted with autoSubscribe: true behaves differently: the retry is answered immediately with a PENDING response, and the terminal follows separately. See Order Subscriptions and Recovery →.
RFS600031 — the Key Was Reused for a Different Order
Section titled “RFS600031 — the Key Was Reused for a Different Order”RFS600031 does not mean “you have already used this key”. It means the key could not be resolved safely: either it was reused for an order that does not match the one it was first used for, or the original order’s record has already aged out.
Zodia Markets compares side, instrument, quantity, quantityAmountType, quoteId and accountGrpUuid; if any of them differs, the request is refused instead of replayed.
{ "timestamp": 1718113605032, "messageType": "error", "message": "Order with this idempotency key already exists", "clientRequestId": "TESTCLIENT1", "code": "RFS600031", "quoteId": "YdB4DodjOC5FkBDrYmKLW/Dwnz+x9bVbXyshrwz8yntjA+kZ...", "transactionId": ""}When you receive this error:
- You are placing a new order, not retrying an old one. Generate a new
idempotencyKeyfor it. - If you meant to retry, the retry differs from the original. Reconcile the two — check the original with Read Order State → or the Get Trades API — before resending.
Key Format
Section titled “Key Format”- 1 to 64 characters.
- No colon (
:), no whitespace and no control characters. Dots are allowed, so an instrument name such asUSDC.AEDcan be embedded in the key. - Keys are scoped to your own credential — they need only be unique within your own account.
- A key that breaks these rules is refused with
RFS100012and the order is not placed. - To send an order without idempotency, omit the field. Do not send
"idempotencyKey": "".
Key Expiration
Section titled “Key Expiration”Keys expire 24 hours after first use. After that, an order carrying the same key is a new order and is executed.
Idempotency vs Client Request ID
Section titled “Idempotency vs Client Request ID”| Field | Purpose | Must be Unique? | Used For |
|---|---|---|---|
clientRequestId |
Track order through your system | Yes (per order) | Correlating responses, reading order state, querying trade history |
idempotencyKey |
Make a retry of one order safe | Yes (per order, 24-hour window) | Recovering the outcome of an order whose send may have failed |
Both fields are important:
- Use
clientRequestIdto identify and track orders in your system. - Use
idempotencyKeyso a retry recovers the first order instead of placing a second one. - They can be the same value, but don’t have to be.
Order Response
Section titled “Order Response”The outcome of your order, on the connection that submitted it. An order response is returned whether the order filled or not — read orderStatus, not the presence of the message.
Response Fields
Section titled “Response Fields”| Field | Type | Required | Description |
|---|---|---|---|
messageType |
string | yes | Always order, whatever the outcome. Do not treat its arrival as success — orderStatus carries that. Note also that a failure can reach you as either an order message with a non-SUCCESS orderStatus or a separate error message, depending on where in the chain it happened and whether it is first delivery or recovery; a client that branches only on messageType will mishandle one of the two. |
timestamp |
integer (int64) | yes | Unix epoch milliseconds, UTC — when this message was built, which is not necessarily when the order was dealt. A terminal redelivered after a reconnect is stamped at redelivery, and a replayed idempotent order carries the stamp of the original. Treat it as a diagnostic, not as a sequence number: it is not monotonic across the messages for one order. |
message |
string | yes | Empty on SUCCESS and PENDING. On every failure it is the same fixed sentence — Please contact your desk support. — never a description of what went wrong. code is the only field that distinguishes one failure from another. |
code |
string | yes | Empty on SUCCESS and PENDING. On failure: RFS600001 the order did not fill and the quote can no longer be dealt; RFS600002 the outcome is unknown; RFS600003 an unexpected internal state. A terminal recovered after a reconnect may instead carry the code recorded when the order originally failed. |
instrument |
string | yes | The pair dealt, BASE.QUOTE, taken from the executed quote. |
side |
string (enum) | yes | Which way the order went, echoed from the request. BUY — you bought the base currency at the quote’s offer.price. SELL — you sold it at the bid.price. — One of: BUY, SELL |
price |
string | yes | The price dealt, in quote currency per one unit of base currency — the price sealed into the quote you executed, not a separately derived fill price. Note: This field can be an empty string on a terminal recovered after a reconnect or replayed under an idempotency key, including a successful one. Treat "" as “not carried on this message”, never as zero — the dealt price is on the trade record. |
quantity |
string | yes | The size dealt, in the currency the underlying subscription was made in — not necessarily the base currency. It always equals the quantity you asked for: orders are all-or-nothing, so this is never a partially filled amount and never less than the request. |
clientRequestId |
string | yes | Your own identifier, echoed from the request. It is the key the order-state read looks orders up by, and it becomes clientRef on the resulting trade. |
orderStatus |
string (enum) | yes | Outcome of the order, and the only field that carries it. SUCCESS filled; FAILED did not fill; INDETERMINATE means the outcome is genuinely unknown and the order may have filled — do not re-place it, reconcile it; PENDING is accepted-not-yet-known and is sent only to clients that set autoSubscribe. Read this field and nothing else to classify an order. — One of: SUCCESS, FAILED, INDETERMINATE, PENDING |
quoteId |
string | yes | The quoteId that was executed, echoed from the request. |
transactionId |
string | yes | Zodia Markets’ reference for this order: 32 lowercase hexadecimal characters, a UUID with the hyphens removed. Assigned before the order is sent onward, so it exists even on orders that never dealt, and it is the identifier that ties the PENDING, the terminal, every redelivery of that terminal and your orderUnsubscribe together. Correlate on it. It is usually absent from error messages, which is why a rejected order can leave you with nothing to key on but clientRequestId. |
settleDate |
string | yes | The date the trade settles, YYYYMMDD, taken from the executed quote — a settleDate sent on the request is ignored. Empty on a terminal recovered after a reconnect or replayed under an idempotency key. |
tenor |
string | yes | The tenor dealt, taken from the executed quote — a tenor sent on the request is ignored. Empty on a terminal recovered after a reconnect or replayed under an idempotency key. |
subscriptionStatus |
string (enum) | Present only on responses to orders sent with autoSubscribe: true, and on those it only ever reads SUBSCRIBED — this message is not the place you observe an order becoming unsubscribed. Once you acknowledge with orderUnsubscribe, no further order message arrives for that order at all. Absent from the wire, not null, for orders sent without autoSubscribe. The other two values appear on the order-state read. — One of: NOT_SUBSCRIBED, SUBSCRIBED, UNSUBSCRIBED |
|
sessionId |
string | The session this order’s outcome is delivered to, and the value to reconnect under to recover it. It is the sessionId from the WebSocket handshake where you supplied one; where you did not, Zodia Markets assigns one and this is the only place it is returned to you. Present only on responses to orders sent with autoSubscribe: true. |
Store the transactionId - use it to track settlement status via REST API.
Order Status
Section titled “Order Status”orderStatus is the outcome field. Branch on all four values.
orderStatus |
Meaning | What to do |
|---|---|---|
SUCCESS |
The order filled at the price and quantity on the response. |
Book the trade. |
FAILED |
The order did not fill. code carries the reason; message is Please contact your desk support. |
Do not book. Re-quote and resubmit if you still want the trade. |
INDETERMINATE |
The outcome is genuinely unknown — the order may have filled. code is RFS600002. |
Do not book it, and do not resubmit it. Confirm the outcome with your desk before acting. |
PENDING |
Accepted, outcome not yet known. Only sent to clients that set autoSubscribe: true; the terminal follows separately. |
Wait for the terminal. See Order Subscriptions and Recovery →. |
price, quantity and settleDate are meaningful only on a SUCCESS response.
Error Response
Section titled “Error Response”Returns error details when order execution fails.
| Field | Type | Required | Description |
|---|---|---|---|
messageType |
string | yes | Always error. This message carries failures that happen before a request becomes a typed exchange — a malformed or refused order, a rate limit, a request type this connection cannot serve. A subscribe never fails this way (it reports failure on its own subscribe response), and an order that reached the market and then failed comes back as an order message instead. |
timestamp |
integer (int64) | yes | Unix epoch milliseconds, UTC — when the error was raised. |
message |
string | yes | What went wrong, in plain text. It is chosen independently of code, so the same code can arrive with different wording and the wording can change without notice — branch on code, and log this. Some messages contain an unsubstituted %s placeholder where a value was meant to be interpolated; that is cosmetic and does not change the meaning of code. |
code |
string | yes | Error code — see the Response / Error Code Reference |
clientRequestId |
string | Your identifier from the request that failed (empty when the failure is not tied to one) | |
quoteId |
string | Quote ID from the failed request (empty when not applicable) | |
transactionId |
string | System-generated transaction reference, when one had already been assigned (empty otherwise) |
Usage Examples
Section titled “Usage Examples”Basic Order Execution
Section titled “Basic Order Execution”import json
def execute_order(ws, quote_id, side, client_request_id): """Execute order on price stream quote""" order = { 'messageType': 'order', 'quoteId': quote_id, 'side': side, 'clientRequestId': client_request_id } ws.send(json.dumps(order)) print(f"Sent {side} order: {client_request_id}")
# Handle price update and executedef on_price_update(ws, msg): if msg['messageType'] == 'pricestream': # Check if price meets criteria bid = float(msg['bid']['price']) offer = float(msg['offer']['price'])
if should_buy(offer): execute_order( ws, msg['quoteId'], 'BUY', f"ORDER-{int(time.time())}" )
# Handle order responsedef on_order_response(ws, msg): if msg['messageType'] == 'order': if msg['orderStatus'] == 'SUCCESS': print(f"✅ Order filled!") print(f" Price: {msg['price']}") print(f" Quantity: {msg['quantity']}") print(f" Transaction ID: {msg['transactionId']}") else: print(f"❌ Order failed: {msg['message']}")
elif msg['messageType'] == 'error': print(f"❌ Error {msg['code']}: {msg['message']}")function executeOrder(ws, quoteId, side, clientRequestId) { const order = { messageType: 'order', quoteId: quoteId, side: side, clientRequestId: clientRequestId }; ws.send(JSON.stringify(order)); console.log(`Sent ${side} order: ${clientRequestId}`);}
// Handle price update and executefunction onPriceUpdate(ws, msg) { if (msg.messageType === 'pricestream') { const bid = parseFloat(msg.bid.price); const offer = parseFloat(msg.offer.price);
if (shouldBuy(offer)) { executeOrder( ws, msg.quoteId, 'BUY', `ORDER-${Date.now()}` ); } }}
// Handle order responsefunction onOrderResponse(ws, msg) { if (msg.messageType === 'order') { if (msg.orderStatus === 'SUCCESS') { console.log('✅ Order filled!'); console.log(` Price: ${msg.price}`); console.log(` Quantity: ${msg.quantity}`); console.log(` Transaction ID: ${msg.transactionId}`); } else { console.error(`❌ Order failed: ${msg.message}`); } } else if (msg.messageType === 'error') { console.error(`❌ Error ${msg.code}: ${msg.message}`); }}Complete Trading Flow
Section titled “Complete Trading Flow”import jsonimport time
# Track orderspending_orders = {}
def handle_message(ws, message): """Complete order flow handler""" msg = json.loads(message)
# 1. Receive price update if msg['messageType'] == 'pricestream': quote_id = msg['quoteId'] offer = float(msg['offer']['price']) bid = float(msg['bid']['price'])
print(f"Price: Bid {bid}, Offer {offer}")
# 2. Decide to trade if should_execute_buy(offer): client_id = f"ORDER-{int(time.time())}"
# 3. Send order order = { 'messageType': 'order', 'quoteId': quote_id, 'side': 'BUY', 'clientRequestId': client_id } ws.send(json.dumps(order))
# Track order pending_orders[client_id] = { 'quote_id': quote_id, 'side': 'BUY', 'sent_at': time.time() }
# 4. Handle order response elif msg['messageType'] == 'order': client_id = msg['clientRequestId']
if msg['orderStatus'] == 'SUCCESS': print(f"✅ Order {client_id} filled") print(f" {msg['side']} {msg['quantity']} {msg['instrument']}") print(f" Price: {msg['price']}") print(f" Transaction: {msg['transactionId']}")
# Remove from pending pending_orders.pop(client_id, None) else: print(f"❌ Order {client_id} failed: {msg['message']}") pending_orders.pop(client_id, None)
# 5. Handle errors elif msg['messageType'] == 'error': client_id = msg.get('clientRequestId') print(f"❌ Error {msg['code']}: {msg['message']}")
if client_id: pending_orders.pop(client_id, None)
def should_execute_buy(price): """Your trading logic here""" return price < 3.67 # Example threshold
# Check for stale ordersdef check_pending_orders(): """Alert on orders that haven't confirmed""" now = time.time() for client_id, order in pending_orders.items(): age = now - order['sent_at'] if age > 5: # 5 seconds timeout print(f"⚠️ Order {client_id} pending for {age:.1f}s")// Track ordersconst pendingOrders = {};
function handleMessage(ws, message) { const msg = JSON.parse(message);
// 1. Receive price update if (msg.messageType === 'pricestream') { const quoteId = msg.quoteId; const offer = parseFloat(msg.offer.price); const bid = parseFloat(msg.bid.price);
console.log(`Price: Bid ${bid}, Offer ${offer}`);
// 2. Decide to trade if (shouldExecuteBuy(offer)) { const clientId = `ORDER-${Date.now()}`;
// 3. Send order const order = { messageType: 'order', quoteId: quoteId, side: 'BUY', clientRequestId: clientId }; ws.send(JSON.stringify(order));
// Track order pendingOrders[clientId] = { quoteId: quoteId, side: 'BUY', sentAt: Date.now() }; } }
// 4. Handle order response else if (msg.messageType === 'order') { const clientId = msg.clientRequestId;
if (msg.orderStatus === 'SUCCESS') { console.log(`✅ Order ${clientId} filled`); console.log(` ${msg.side} ${msg.quantity} ${msg.instrument}`); console.log(` Price: ${msg.price}`); console.log(` Transaction: ${msg.transactionId}`);
delete pendingOrders[clientId]; } else { console.error(`❌ Order ${clientId} failed: ${msg.message}`); delete pendingOrders[clientId]; } }
// 5. Handle errors else if (msg.messageType === 'error') { const clientId = msg.clientRequestId; console.error(`❌ Error ${msg.code}: ${msg.message}`);
if (clientId) { delete pendingOrders[clientId]; } }}
function shouldExecuteBuy(price) { return price < 3.67; // Example threshold}Best Practices
Section titled “Best Practices”Client Request IDs
Section titled “Client Request IDs”# ✅ Good - Unique, traceable IDsimport uuidimport time
def generate_order_id(): # Timestamp + UUID return f"ORD-{int(time.time())}-{str(uuid.uuid4())[:8]}"
client_id = generate_order_id()# Example: "ORD-1737552000-a1b2c3d4"
# ❌ Bad - Non-unique IDsclient_id = "ORDER1" # Reused across ordersQuote Freshness
Section titled “Quote Freshness”# ✅ Good - Use latest quotedef on_price_update(msg): current_quote = msg['quoteId'] # Use immediately execute_order(current_quote, 'BUY', ...)
# ❌ Bad - Stale quotesstored_quote = None
def on_price_update(msg): global stored_quote stored_quote = msg['quoteId'] # Storing for later
def execute_later(): # Quote likely expired execute_order(stored_quote, 'BUY', ...)Error Handling
Section titled “Error Handling”# ✅ Good - Handle every orderStatus valuedef handle_order_response(msg): if msg['messageType'] == 'error': handle_error(msg) elif msg['orderStatus'] == 'SUCCESS': handle_successful_order(msg) elif msg['orderStatus'] == 'FAILED': handle_failed_order(msg) elif msg['orderStatus'] == 'INDETERMINATE': # Outcome unknown - the order may have executed. Do not re-place it. escalate_to_desk(msg) elif msg['orderStatus'] == 'PENDING': # Subscribed orders only - the terminal follows separately. await_terminal(msg['transactionId'])
# ❌ Bad - Only checking successdef handle_order_response(msg): if msg['orderStatus'] == 'SUCCESS': process_fill(msg) # Ignoring failures, unknown outcomes and errorsUse Idempotency Keys
Section titled “Use Idempotency Keys”import uuidfrom datetime import datetime
# ✅ Good - one key per order, generated once, reused on every retry of that orderdef generate_idempotency_key(instrument, side): """Generate one idempotency key for one order""" timestamp = datetime.utcnow().strftime('%Y%m%d-%H%M%S') unique_id = str(uuid.uuid4())[:8] return f"ORDER-{timestamp}-{instrument}-{side}-{unique_id}"
# Usageidempotency_key = generate_idempotency_key('USDC.AED', 'BUY')# Example: "ORDER-20260420-102535-USDC.AED-BUY-a1b2c3d4"
order = { 'messageType': 'order', 'quoteId': quote_id, 'side': 'BUY', 'clientRequestId': 'ORDER-123', 'idempotencyKey': idempotency_key}
# ❌ Bad - one key shared by every orderidempotency_key = "MY-ORDER-KEY" # Second order is refused with RFS600031
# ❌ Bad - a fresh key on each retry of the same orderfor attempt in range(3): order['idempotencyKey'] = generate_idempotency_key('USDC.AED', 'BUY') ws.send(json.dumps(order)) # Each attempt is a new orderRetry Logic with Idempotency
Section titled “Retry Logic with Idempotency”import time
def execute_order_with_retry(ws, quote_id, side, max_retries=3): """Execute order, retrying the send with the SAME key until an outcome is known"""
# Generate once - the same key is what makes every retry safe client_id = f"ORDER-{int(time.time())}" idempotency_key = f"IDEMPOTENT-{client_id}"
order = { 'messageType': 'order', 'quoteId': quote_id, 'side': side, 'clientRequestId': client_id, 'idempotencyKey': idempotency_key }
for attempt in range(max_retries): try: ws.send(json.dumps(order)) print(f"Order sent (attempt {attempt + 1})")
response = wait_for_order_response(client_id, timeout=5)
if response is None: continue # No answer - resend the identical order, same key
if response['messageType'] == 'order': # A replay of the original order looks exactly like a first response. # transactionId identifies the one order that was executed. print(f"Outcome {response['orderStatus']}: {response['transactionId']}") return response
if response['code'] == 'RFS100012' and 'still processing' in response['message']: time.sleep(1) continue # Original is in flight - retry with the same key
if response['code'] == 'RFS600031': # This key was first used for a DIFFERENT order. Do not resend # blindly: reconcile against the original before acting. raise IdempotencyKeyConflict(response)
print(f"❌ Order refused: {response['message']}") return response
except ConnectionError as e: print(f"Connection error on attempt {attempt + 1}: {e}") if attempt < max_retries - 1: time.sleep(1) else: raise
return None