A trading session is one WebSocket connection authenticated by one Ed25519 API key. It trades for
exactly one account, numbers its commands from 1, and ends when the socket closes. Nothing carries
over to the next connection except the effects of the commands it sent.
429 responses and close codes instead of hard-coding the values.Sign the upgrade
The server authenticates the HTTP upgrade request itself. There is no login message after the
socket opens, and JWT sessions are not accepted: the upgrade must carry API-key headers and no Authorization header. Send each header once.
Send the three headers from Ed25519 API
keys on the GET upgrade request:
| Header | Value |
|---|---|
X-API-KEY-ID | The key ID returned by CreateApiKey |
X-API-TIMESTAMP | Current Unix time in milliseconds, within 10 seconds of Polyester time |
X-API-SIGNATURE | Ed25519 signature of the canonical string, as hex or standard padded Base64 |
The canonical string for the upgrade has an empty query and the SHA-256 of an empty body:
1762171200000
GET
/ws/trade.sbe
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855The first line is your X-API-TIMESTAMP value. The fourth line is empty because the upgrade URL
has no query. Sign every connection attempt with a fresh timestamp: the server remembers recent
signatures and rejects a repeated signed upgrade as SIGNATURE_INVALID.
If the key has an IP allowlist, the address you connect from must be on it. See API key IP allowlists.
Which account the session trades for
The session acts for the account the key is bound to:
- a Main Account key trades for the Main Account
- a Subaccount-bound key trades for that Subaccount
Commands carry no account or Subaccount field, and the session receives private events of that one account only. To trade several Subaccounts, open one connection per Subaccount with a key bound to each. Self-trade prevention still applies across the Main Account and all its Subaccounts; see Self-trade prevention.
Upgrade failures
A refused upgrade is a plain HTTP response, not a WebSocket close. Its body is the code as text, followed by a newline.
UNAUTHENTICATED· HTTP 401
No API-key headers, or anAuthorizationheader. Send only the three API-key headers.MISSING_HEADERS· HTTP 401
A key ID without a timestamp or signature. Send all three headers.TIMESTAMP_SKEW· HTTP 401
Timestamp malformed or more than 10 seconds from server time. Sync the clock and sign again.SIGNATURE_INVALID· HTTP 401
Unknown key, bad signature, or a repeated signed upgrade. Fix the canonical string and sign a fresh one.KEY_DISABLED· HTTP 401
The key is disabled or revoked. Use an active key.KEY_EXPIRED· HTTP 401
The key has expired. Use an active key.IP_NOT_ALLOWED· HTTP 401
Your address is outside the key's IP allowlist. Connect from an allowed address.TOO_MANY_SESSIONS_API_KEY· HTTP 429
The key holds its maximum of concurrent sessions. Close a session of the key, or use another key.TOO_MANY_SESSIONS_ACCOUNT· HTTP 429
The account holds its maximum of concurrent sessions. Close one of the account's sessions.- Empty or non-Polyester body · HTTP 429
Too many open connections from your IP address. Close connections before opening more.
Connection limits
Up to 16 concurrent sessions per API key and 64 per account are always accepted. Above those
numbers an upgrade may be refused with 429. The gateway also limits one IP address to 200 open
connections to this endpoint.
A market maker typically spreads strategies over a few sessions per key and a few keys per account. Sessions do not share sequence numbers or subscriptions: each is independent.
Frames
- Send exactly one SBE message per binary WebSocket frame. A text frame closes the connection with
1003. - A message may not exceed 64 KiB. A larger frame closes the connection with
1009. The largest valid command, a fullBatchCreateOrders, is far below that. - WebSocket compression is not negotiated. Send uncompressed frames.
Schemas and codecs describes the message inside each frame.
Sequence numbers
Every command carries seq, the connection's client sequence number. The first command on a
connection uses 1, and each command the server takes uses the next number: 2, 3, and so on.
A new connection starts again at 1.
A command whose seq is not the next expected number is answered with a CommandReject with
code SEQUENCE_OUT_OF_ORDER (1001) and is not processed. The connection stays open, and the
expected number does not change.
A command rejected before sequencing does not use its number either. That covers a frame the server
cannot decode and a command that fails its structural checks, such as a missing required enum, a 0 sequence, or a batch above its item limit. Every other answer, an acknowledgement or a reject,
uses the number.
When you are unsure whether a rejected command used its number N, send a Ping with seq N while nothing else is in flight. Either it is answered with Pong and uses N, or it is rejected
with SEQUENCE_OUT_OF_ORDER because N was already used. In both cases your next command uses N + 1.
Correlation IDs
correlationId is your own 64-bit token, copied unchanged into the answer. Use it to match answers
to commands. For ModifyOrder, CancelAllOrders, and the three batch commands it is also the
command's idempotency identity: it must be nonzero, and a retry keeps it while a new operation uses
a new one. See Order entry.
Pipelining and backpressure
You do not need to wait for an answer before sending the next command. The server takes commands in sequence order, dispatches each as soon as it is admitted, and answers them in the order it read them.
Up to 1,024 commands can be unanswered on one connection. At that point the server stops reading the socket until an answer is written, so TCP flow control holds your sends back. Nothing is rejected for this reason.
The account also has a limit of outstanding order requests shared by all its connections. A command
above it is rejected with OVERLOADED. See Errors and rate limits.
Keepalive
The server sends a WebSocket ping control frame every 20 seconds. Standard WebSocket libraries
answer it automatically with a pong. If the server neither receives a pong nor completes a write to
you within 60 seconds, it closes the connection with 1008 (pong_timeout). A write to you that
cannot complete within 10 seconds closes it with 1008 (write_timeout).
Keep reading the socket continuously, even when you are not sending. Reading is also how the server's pings and your private events drain.
To measure round-trip time or check liveness from your side, send the SBE Ping command. Its Pong echoes seq, correlationId, and up to the frame limit of opaque payload bytes. A Ping uses a sequence number like any other command.
Close codes
Branch on the close code. The close reason is diagnostic text and may change.
1003·binary frames required
You sent a text frame. Fix the client; send binary frames only.1008·pong_timeout; reconnect and resync required
No pong or completed write within 60 seconds. Reconnect and reconcile.1008·write_timeout; reconnect and resync required
Your client stopped reading. Read faster, then reconnect and reconcile.1008·private event queue overflow; resync required
Your private events backed up because your client read too slowly. Read faster, then reconnect and reconcile.1009· library-defined reason text
A frame exceeded 64 KiB. Fix the client.1012·private event stream gap; resync required
Private events of your account, or of a Pair withoutmarketResync, may be missing. Reconnect and reconcile.1012·private event source unavailable; resync required
Private event delivery is temporarily unavailable. Reconnect with backoff and reconcile.1013·command without a definite outcome; reconcile before resending
A command was answered with an unknown outcome, or could not be sent. Reconcile every unanswered command, then reconnect.1006· no close frame, so no reason text
Network failure or server restart. Reconnect with backoff and reconcile.
Without marketResync, a 1012 can follow a missed event of any account on a Pair, including a
Pair you do not trade. With marketResync enabled, a 1012 still arrives when events of your own
account may be missing, and when private event delivery is unavailable. Handle 1012 on every
connection. See Responses and events.
Outcome-unknown close
When the server sends a command but cannot confirm its outcome, it answers that command with an
outcome-unknown CommandReject and then closes with 1013. A command it could not send is answered
with UNAVAILABLE and closes the connection the same way. It does not send or answer any command
it read after that one. Commands you pipelined behind it get no answer and may or may not have been
applied.
1013, read the state of every command that has no answer before you resend anything: read
each order by client order ID with GetOrder, or read all open
orders. Resending blindly can place an order twice. Batch
commands can be resent with the same correlationId and items to learn each item's result.Reconnect
Wait with backoff
Reconnect after a short delay that grows with each failed attempt, with jitter. For example, start
at 0.5 seconds and double up to 10 seconds. Do not retry a 401 until its cause is fixed.
Sign a fresh upgrade
Use a new timestamp and signature. Start the new connection at seq 1.
Subscribe again
Subscriptions do not survive the connection. Send Subscribe before relying on events.
Reconcile
Read open orders with GET /v1/orders/open and recent
executions with GET /v1/spot/trades, or their ConnectRPC
equivalents GetOpenOrders and GetUserTrades. Resolve every
command that was unanswered when the connection closed. Then apply new events, ignoring order events
whose version is not newer than the state you loaded.
Resume trading
Resend only the commands your reconciliation shows did not take effect. Keep the same correlationId for a retried modify, cancel-all, or batch.