SBE WebSocket is the specialized path for clients that prioritize minimal encoding and transport overhead for order entry. It uses Simple Binary Encoding in binary WebSocket frames.
One authenticated connection carries your order commands, their acknowledgements, and, when you subscribe, the private order and execution events of the account the connection trades for.
When to use SBE WebSocket
Use this interface for latency-sensitive trading systems that:
- maintain long-lived authenticated sessions
- encode and decode fixed-layout binary messages
- manage strict client sequencing and backpressure
- operate REST, ConnectRPC, or realtime subscriptions alongside order entry
Use an official SDK with ConnectRPC when typed API access and broader service coverage matter more than the smallest order-entry wire format. See the Connectivity matrix for the role of each interface.
Endpoints
Open the socket on the trading host, not on the API host:
| Environment | Endpoint |
|---|---|
| Testnet | wss://trade.api.testnet.polyester.com/ws/trade.sbe |
| Mainnet | Coming soon |
The API host api.testnet.polyester.com does not serve this socket. An upgrade without valid
API-key headers is refused with HTTP 401 and the body UNAUTHENTICATED. The interface has no
JSON encoding. A text frame closes the connection.
The SBE market-data stream is not available yet. Use WebSocket Protobuf for public market data.
How a session works
| Step | What happens | Details |
|---|---|---|
| Sign the upgrade | Ed25519 API-key headers authenticate the WebSocket upgrade | Connection and authentication |
| Send commands | One SBE message per binary frame, each with the next sequence number | Schemas and codecs |
| Trade | Create, cancel, modify, batch, and cancel-all commands | Order entry |
| Read answers and events | One answer per command, in command order, plus subscribed order and execution events | Responses and events |
| Handle rejects and limits | Rejections keep the connection open; a few outcomes close it | Errors and rate limits |
| Look up a field | Every template, field, and enum | Message reference |
Schema version
Generate your client from the schema published for the environment you connect to:
| Environment | Published SBE schema | Accepted schema version |
|---|---|---|
| Testnet | Testnet schema files · Testnet manifest | tradeWebSocketSbe.schemaVersion in the manifest; also the XML version |
| Mainnet | Coming soon | Coming soon |
The manifest's paths.sbe names the exact XML file on the testnet branch. Use the testnet branch rather than any other branch of the repository: only that branch matches what Testnet
accepts.
The following command downloads the Testnet XML and prints its schema ID, accepted version, and exact file URL. It pins both downloads to one commit, so a release during the download cannot mix the manifest and XML.
branch=testnet
revision=$(git ls-remote https://github.com/Fabric-Labs/polyester-proto.git "refs/heads/$branch" | cut -f1)
python3 - "$revision" <<'PYTHON'
import hashlib
import json
import sys
import urllib.request
import xml.etree.ElementTree as ET
from pathlib import Path
revision = sys.argv[1]
if not revision:
raise SystemExit("Branch not found")
base = f"https://raw.githubusercontent.com/Fabric-Labs/polyester-proto/{revision}"
with urllib.request.urlopen(f"{base}/manifest.json") as response:
manifest = json.load(response)
schema_url = f"{base}/{manifest['paths']['sbe']}"
with urllib.request.urlopen(schema_url) as response:
schema = response.read()
metadata = manifest["tradeWebSocketSbe"]
root = ET.fromstring(schema)
if (hashlib.sha256(schema).hexdigest() != metadata["sha256"]
or int(root.attrib["id"]) != metadata["schemaId"]
or int(root.attrib["version"]) != metadata["schemaVersion"]):
raise SystemExit("Schema and manifest do not match")
Path("tradews.xml").write_bytes(schema)
print(f"Schema ID: {root.attrib['id']}; accepted version: {root.attrib['version']}")
print(f"Schema file: {schema_url}")
PYTHONGenerate codecs from the downloaded tradews.xml. Use the XML's messageSchema ID and version in
the SBE message header, and use your generated codec's block length for each message. A field's sinceVersion says when that field was introduced; it does not select a server-supported message
version.
The schema ID is 7. The server accepts only its exact schema version: a message whose header
carries another version is answered with a CommandReject with code BAD_REQUEST and the detail
"unexpected SBE schema version", and is not processed. Refresh the manifest and regenerate your
client when the Testnet schema changes. Schemas and
codecs covers code generation per language
and the wire rules every message follows.
Quickstart
This walkthrough places one post-only limit order and cancels it. It needs an Ed25519 API key with trading
permission on a funded account, and the tradews.xml downloaded above.
Pick a Pair and scale your values
Read the Pair configuration with GET /v1/spot/config (ConnectRPC GetSpotConfig)
and note the Pair's symbolId and baseQuantityScale. Prices are always in ticks of
10-9 quote units: a price of 65000.5 is priceTicks 65000500000000. A quantity of 0.01 with baseQuantityScale 8 is baseQtyScaled 1000000. See Scaled
integers. Choose a sell price well above the
market so the order rests.
Sign the WebSocket upgrade
Authenticate the WebSocket upgrade with the Ed25519 API-key headers described in Ed25519 API keys. Sign GET /ws/trade.sbe with an empty query and the hash of an empty body. The session is bound to the
key's Main Account or Subaccount scope.
Start the connection sequence
Begin at sequence 1. Every subsequent command on the same connection must use the next contiguous
sequence number and a client correlation value.
Send Ping first
Encode one supported command with the schema for your environment and send it
in a binary WebSocket frame. Every message header carries that schema's ID and version. Start with Ping and verify that Pong echoes the sequence, correlation value, and payload before sending
order commands.
Subscribe, create, and cancel
Send Subscribe with the executions and orders channels, then CreateOrder, then CancelOrder by the same client order ID.
Handle the response
Match the echoed sequence and correlation values to the command. Handle the command acknowledgement,
batch acknowledgement, command rejection, or pong response. A rejection does not close the
connection. A rate-limit rejection carries the quota details and the minimum retry delay in its rateLimit* fields; for any other rejection they are null or zero.
Reconcile application state
After reconnecting or receiving an uncertain command outcome, reload state before resuming
decisions, for example with GET /v1/orders/open (ConnectRPC GetOpenOrders). If
you subscribed to private order or execution events and the server detects that some of a Pair's
events may have been missed, it closes the connection with code 1012: reconnect and reload open
orders before applying new events. Enable marketResync in Subscribe to receive a MarketResync for that Pair instead and keep the connection open. Reconcile that Pair's orders and executions over
REST or ConnectRPC, then apply its later events.
The following Python client performs those steps. It encodes the messages by hand with struct so
that every byte is visible; production clients should use generated codecs. It needs Python 3.11
or later and pip install websockets cryptography (websockets 14 or later).
import asyncio
import os
import struct
import time
import xml.etree.ElementTree as ET
import websockets
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
HOST = os.environ.get("POLYESTER_TRADE_HOST", "trade.api.testnet.polyester.com")
PATH = "/ws/trade.sbe"
EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
# Read the schema ID and version from the XML downloaded for this environment.
SCHEMA = ET.parse("tradews.xml").getroot()
SCHEMA_ID, SCHEMA_VERSION = int(SCHEMA.get("id")), int(SCHEMA.get("version"))
HEADER = struct.Struct("<HHHH") # blockLength, templateId, schemaId, version
INT64_NULL = -(2**63)
PING, CREATE_ORDER, CANCEL_ORDER, SUBSCRIBE = 1, 2, 3, 6
COMMAND_ACK, COMMAND_REJECT, PONG, SUBSCRIPTION_ACK, ORDER_EVENT = 20, 21, 22, 23, 25
# CreateOrder fixed block, 82 bytes: seq, correlationId, symbolId, side, orderType,
# timeInForce, baseQtyScaled, priceTicks, flags, feeAsset, stpMode,
# marketClientRefPriceTicks, marketMaxSlippageTicks, marketMaxSlippageBps,
# sizingKind, maxQuoteDebitScaled, expireAtTsNs
CREATE_BLOCK = struct.Struct("<QQIBBBqqIBBqqiBqq")
def frame(template_id: int, block: bytes, *var_data: bytes) -> bytes:
out = HEADER.pack(len(block), template_id, SCHEMA_ID, SCHEMA_VERSION) + block
for data in var_data:
out += struct.pack("<I", len(data)) + data
return out
def ping(seq: int, correlation_id: int, payload: bytes) -> bytes:
return frame(PING, struct.pack("<QQ", seq, correlation_id), payload)
def subscribe(seq: int, correlation_id: int, channels: int) -> bytes:
return frame(SUBSCRIBE, struct.pack("<QQI", seq, correlation_id, channels))
def post_only_limit_sell(seq, correlation_id, symbol_id, qty_scaled, price_ticks, client_order_id):
block = CREATE_BLOCK.pack(
seq, correlation_id, symbol_id,
2, # side SELL
1, # orderType LIMIT
1, # timeInForce GTC
qty_scaled, price_ticks,
1, # flags: postOnly
1, # feeAsset QUOTE
1, # stpMode EXPIRE_MAKER
INT64_NULL, INT64_NULL, -(2**31), # no market price protection overrides
1, # sizingKind BASE_QUANTITY
INT64_NULL, # maxQuoteDebitScaled is null
INT64_NULL, # expireAtTsNs is null for GTC
)
return frame(CREATE_ORDER, block, client_order_id.encode("ascii"))
def cancel_by_client_order_id(seq, correlation_id, symbol_id, client_order_id):
block = struct.pack("<QQQI", seq, correlation_id, 0, symbol_id) # orderId 0: use clientOrderId
return frame(CANCEL_ORDER, block, client_order_id.encode("ascii"))
def describe(msg: bytes) -> str:
block_length, template_id, _, _ = HEADER.unpack_from(msg)
body = msg[HEADER.size:]
if template_id == COMMAND_ACK:
seq, corr, command, order_id, symbol_id = struct.unpack_from("<QQBQI", body)
return f"CommandAck seq={seq} correlationId={corr} commandType={command} orderId={order_id}"
if template_id == COMMAND_REJECT:
seq, corr, code = struct.unpack_from("<QQH", body)
(length,) = struct.unpack_from("<I", body, block_length)
detail = body[block_length + 4:block_length + 4 + length].decode()
return f"CommandReject seq={seq} correlationId={corr} code={code} detail={detail!r}"
if template_id == PONG:
seq, corr = struct.unpack_from("<QQ", body)
return f"Pong seq={seq} correlationId={corr}"
if template_id == SUBSCRIPTION_ACK:
seq, corr, channels = struct.unpack_from("<QQI", body)
return f"SubscriptionAck seq={seq} channels={channels:#x}"
if template_id == ORDER_EVENT:
symbol_id, order_id, version, status = struct.unpack_from("<IQIB", body)
return f"OrderEvent orderId={order_id} version={version} status={status}"
return f"template {template_id}, {len(msg)} bytes"
def upgrade_headers(key_id: str, private_key: Ed25519PrivateKey) -> dict:
timestamp = str(int(time.time() * 1000))
canonical = f"{timestamp}\nGET\n{PATH}\n\n{EMPTY_BODY_SHA256}"
signature = private_key.sign(canonical.encode()).hex()
return {"X-API-KEY-ID": key_id, "X-API-TIMESTAMP": timestamp, "X-API-SIGNATURE": signature}
async def main() -> None:
private_key = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(os.environ["POLYESTER_API_PRIVATE_KEY_HEX"]))
headers = upgrade_headers(os.environ["POLYESTER_API_KEY_ID"], private_key)
symbol_id = int(os.environ["SYMBOL_ID"])
qty_scaled, price_ticks = int(os.environ["QTY_SCALED"]), int(os.environ["PRICE_TICKS"])
client_order_id = f"quickstart-{int(time.time())}"
async with websockets.connect(f"wss://{HOST}{PATH}", additional_headers=headers, compression=None) as ws:
await ws.send(ping(1, 100, b"hello"))
print(describe(await ws.recv()))
await ws.send(subscribe(2, 101, 0b011)) # executions and orders
print(describe(await ws.recv()))
await ws.send(post_only_limit_sell(3, 102, symbol_id, qty_scaled, price_ticks, client_order_id))
print(describe(await ws.recv()))
await ws.send(cancel_by_client_order_id(4, 103, symbol_id, client_order_id))
try: # the cancel acknowledgement, then the order's events
while True:
print(describe(await asyncio.wait_for(ws.recv(), timeout=5)))
except TimeoutError:
pass
if __name__ == "__main__":
asyncio.run(main())Set POLYESTER_API_KEY_ID, POLYESTER_API_PRIVATE_KEY_HEX (the 32-byte Ed25519 private key seed
in hex), SYMBOL_ID, QTY_SCALED, and PRICE_TICKS, then run it from the folder that holds tradews.xml. The client connects to Testnet; POLYESTER_TRADE_HOST overrides the host.
A successful run prints a Pong, a SubscriptionAck with channels 0x3, and a CommandAck with commandType=2 and the new orderId. The loop then prints the cancel's CommandAck (commandType=3) and the order's events in arrival order, for example an OrderEvent with status 4 (WORKING) and, after the cancel, status 6 (CANCELED). An event never precedes the
acknowledgement of the command that caused it; see Ordering.
int64 field must carry its null value, -9223372036854775808, when unused. A
zero-filled buffer sends 0, which the server reads as a value: a MARKET order with priceTicks 0 is rejected. Generated codecs expose each field's null value.