Prefer WebSocket for order submission. Placing and cancelling orders over the persistent
/ws socket is lower-latency (one round trip instead of the REST submit/poll pair) and simpler to build: one connection, one auth handshake, and pushed order_update/fill events instead of polling. See Order Execution over WebSocket.Connecting
Authentication
Authenticate during the WebSocket upgrade using one of two methods. The presence of anX-Client-Id header switches the server into API-key mode; otherwise it expects a JWT.
API Key Authentication:
Include credentials as HTTP headers on the upgrade request:
X-Client-Id is present but X-Api-Key or X-Api-Secret is missing or invalid, the upgrade is rejected with 401 Unauthorized.
JWT Authentication:
Use the standard Authorization header if your client supports it (non-browser clients):
Sec-WebSocket-Protocol header instead:
= padding. A Bearer.<standard base64> form is also accepted. The server echoes
Sec-WebSocket-Protocol: authorization on the 101 response. JWTs that are
expired or revoked (via /auth/logout) are rejected at upgrade time and
revalidated while the connection remains open — see
Errors & Disconnection.
Welcome Message
On successful connection, the server sends:polymarket_builder_code is the public attribution value required by the
one-shot submit_signed_order flow. Include the identical value in the request
and in the signed EIP-712 Order.builder field.
All messages on this WebSocket are JSON text frames. Events are filtered per-user; you only receive events for your own orders and positions.
A complete connect example
API-key event filtering
API-key connections receive an event only when the key has the required scope and, for provider-scoped events, platform access to that event’s originating provider.Server Events
status_changed
Sent whenever an order transitions between statuses. Includes full order details so the frontend can update instantly without an HTTP fetch.holding_wallet is the wallet that will hold the order’s shares. It matters when one market is held in two wallets (a Kairos wallet plus an imported predict.fun account, or a Polymarket EOA plus a Safe): those are two positions, and without this field an order shows under both.
A failing transition may also carry error_code, error_classification, and a
structured error_details object (the same contract as the REST
POST /orders error response). All three are omitted when absent rather than
sent as null.
settlement_pending: true identifies an early off-chain match (currently
predict.fun). Its quantities are provisional: show match feedback, but do not
create a position or make those shares sellable. Authoritative status frames
send false; older servers omit the field. A raw partial status alone cannot
distinguish an early match from a settled partial fill.
partially_filled
Sent when part of your order fills. The order status becomespartial.
filled
Sent when an individual fill occurs on your order. Each fill has its ownfill_id; partial fills from the same order share the
same order_id.
fee is the Kairos platform fee for this fill. exchange_fee is the fee
charged by the venue, normalized to USD. Both are non-negative decimal strings.
exchange_fee is null when the venue does not report its fee on the live fill;
the trade-history response is updated when reconciliation obtains it.
position_updated
Sent after a fill to reflect your updated position, including unrealized PnL.Gotcha: position identity is
(exchange_id, market_id, token_id, holding_wallet). The same market held in two wallets is two positions — a
sell routes to one wallet — so keying on market/token alone merges the rows
and double-counts shares. holding_wallet is null only for legacy rows
with no holder recorded.last_trade_at_ms is the millisecond-precision twin of the second-truncated last_trade_at; same-second updates are unorderable without it.
balance_updated
Sent when your wallet balance changes (after fills, deposits, etc.). Events with atx_hash indicate on-chain balance changes; events without tx_hash are internal adjustments from trade execution.
token is "usdc" or "pusd" on Polygon (chain: "polygon"), or "usdc" on Solana (chain: "solana"). "pusd" is the Polymarket V2 trading collateral — track pUSD balance events for up-to-date buying power.
failed
Sent when an order fails to execute.setup_completed
Sent when an exchange setup (wallet preparation, token approvals) completes.position_redeemed
Sent when a resolved market position is redeemed on-chain.Match on
token_id. condition_id is frequently empty on Polymarket
position rows, so matching on it alone leaves a stale Redeem button.position_resolved
Sent the instant an on-chain resolution is applied to a position you still hold. This is the only signal for a resolution that does not involve a redemption —position_updated carries no resolved flag and
position_redeemed fires only on an actual redeem.
redeemable reflects the post-resolution state (a winner is true). For a
retired loser net_size is "0" and realized_pnl carries the booked loss.
Both are decimal strings.
ctf_action_completed
Sent when a conditional-token split or merge completes on-chain.fill_hint
Optimistic, non-authoritative fill hint emitted from low-latency signal paths (e.g. mempool / preconfirmation watchers). Use it to nudge the UI early; do not treat it as a confirmed fill. A subsequentfilled / partially_filled / status_changed event is the authoritative confirmation.
idempotency_key. If no confirming event arrives within a reasonable window, treat the hint as expired and roll back any optimistic UI state.
notification
In-app notification pushed to the connected client.Event Types Reference
Keepalive
Send a ping to keep the connection alive or measure latency:Ping frames every 30 seconds to keep the connection alive through load balancers (the AWS load balancer idles a connection out at 60 seconds). Your WebSocket library should respond with Pong automatically; if it doesn’t, the server will eventually drop the connection. The server likewise answers a client-sent protocol-level Ping with a Pong.
The same 30-second tick is where the server re-checks JWT expiry and session
revocation, so a revoked identity is cut within one interval.
Delivery, ordering, and recovery
The event stream is a low-latency hint, not a ledger. REST (GET /orders,
GET /positions) is the source of truth whenever you need certainty.
Order Lifecycle Examples
Limit order that fills over timequeued, locked, executing, orphaned) are reported as pending in REST API responses but may appear as distinct status_changed transitions on the WebSocket.
Multiple Connections
If you have multiple connections open for the same user (e.g. across several tabs, processes, or devices), every connection receives every event for your user. Cross-instance relaying is de-duplicated by origin so one event is not fanned out twice.Gotcha: events are not deduplicated per client. There is no per-client
delivery ledger, so the same event can reach you more than once. Dedupe on
the identifiers the events carry —
fill_id, order_id, and
idempotency_key on fill_hint / notification — rather than assuming
exactly-once delivery.Dropped events under lag
Gotcha: a slow reader silently loses events. Events are delivered from a
bounded in-process broadcast channel. A connection that reads too slowly is
not disconnected — it silently misses the events that aged out while it
was behind, and delivery resumes with the newest ones. There is no gap marker
on the wire, so reconcile over REST whenever correctness matters.
Errors & Disconnection
The order execution WebSocket can reject the upgrade, push a per-command error, or close the socket at any time. Clients MUST handle all three cases and reconnect with backoff.Upgrade-time rejections (HTTP status before 101)
The response body is JSON:
{"error":"<reason>", ...}. The connection-limit
429 additionally carries max_allowed and current_count.
Per-command errors (order_error frame)
Commands that fail (validation error, scope check, missing order, exchange error, etc.) return an order_error JSON frame on the same connection — see Order Execution → submit_order error response. The connection normally stays open; only the offending command is rejected. An expired/revoked JWT detected before a command is the exception: the server sends a 401 order_error and terminates the connection. Always correlate the response with your command via request_id.
Programmatic clients should branch on error_details.code (when present) rather than the human-readable error string.
Malformed client messages
Invalid JSON and a missing or unknowntype are silently ignored. If a
recognized command has an invalid envelope — most commonly a missing or
non-string request_id — you receive:
Server-initiated close codes
Gotcha: there is no custom auth close code on this socket. The periodic
heartbeat detects an expired/revoked JWT within 30 seconds and sends
Close(None). A pre-command check instead sends a 401 order_error, then
terminates the connection without explicitly sending a close frame. In both
cases, refresh the JWT before reconnecting.Recommended reconnect / resync flow
- On any close, wait at least 500 ms before reconnecting; on repeated failures use exponential backoff up to 30 s with jitter.
- If reconnect returns
401, refresh your JWT (or API key) before the next attempt. - After reconnecting, call
GET /orders?status=live(andGET /positions) to reconcile any events that fired while you were disconnected — the WebSocket does not replay missed events. - Continue matching incoming events to in-flight commands by
request_idandclient_order_id; idempotency on the server means re-submitting with the sameclient_order_idis safe.
Limits
Connection Limits
Connections are counted per user, so two API keys owned by the same account
share one budget. Exceeding the limit returns
429 Too Many Requests on upgrade
with a body of {"error":"Maximum WebSocket connections exceeded","max_allowed":N,"current_count":N}.
Best Practices
- Always listen on the WebSocket for order status. Do not poll
GET /ordersin a tight loop. - The
status_changedevent is the primary status notification.filledandpartially_filledprovide fill-specific details. - Handle reconnections gracefully. On reconnect, call
GET /orders?status=liveto sync state for any events missed during the disconnection. - Listen for
notificationevents to display user-facing alerts (e.g., fill confirmations, errors). - Use
client_order_idon submissions and match it to WebSocket events for reliable tracking.

