Skip to content

Subscribe to Order Channel

Once prices are streaming, a quote can be executed using the order message

Execute trades instantly on live prices from your price stream subscriptions →.

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.




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.

Execute an order using a quote ID from a price stream update →.

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

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)

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 clientRequestId you sent on the retry and carrying the original order’s transactionId.
  • 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.
{
"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"
}

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 idempotencyKey for 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.
  • 1 to 64 characters.
  • No colon (:), no whitespace and no control characters. Dots are allowed, so an instrument name such as USDC.AED can 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 RFS100012 and the order is not placed.
  • To send an order without idempotency, omit the field. Do not send "idempotencyKey": "".

Keys expire 24 hours after first use. After that, an order carrying the same key is a new order and is executed.

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 clientRequestId to identify and track orders in your system.
  • Use idempotencyKey so a retry recovers the first order instead of placing a second one.
  • They can be the same value, but don’t have to be.


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.

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.

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.


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)

Python
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 execute
def 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 response
def 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']}")


Python
import json
import time
# Track orders
pending_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 orders
def 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")

Python
# ✅ Good - Unique, traceable IDs
import uuid
import 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 IDs
client_id = "ORDER1" # Reused across orders
Python
# ✅ Good - Use latest quote
def on_price_update(msg):
current_quote = msg['quoteId']
# Use immediately
execute_order(current_quote, 'BUY', ...)
# ❌ Bad - Stale quotes
stored_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', ...)
Python
# ✅ Good - Handle every orderStatus value
def 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 success
def handle_order_response(msg):
if msg['orderStatus'] == 'SUCCESS':
process_fill(msg)
# Ignoring failures, unknown outcomes and errors
import uuid
from datetime import datetime
# ✅ Good - one key per order, generated once, reused on every retry of that order
def 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}"
# Usage
idempotency_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 order
idempotency_key = "MY-ORDER-KEY" # Second order is refused with RFS600031
# ❌ Bad - a fresh key on each retry of the same order
for attempt in range(3):
order['idempotencyKey'] = generate_idempotency_key('USDC.AED', 'BUY')
ws.send(json.dumps(order)) # Each attempt is a new order
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