Account-State Stream
A real-time, server-to-server WebSocket stream of your governed accounts’ state.
Connect once, subscribe to the accounts you want, and receive a snapshot on
subscribe plus change-driven updates: an equity tick every 1–2 seconds (only when
it changes), a trade frame for every new fill within one tick, position
updates, and account-lifecycle events.
WS /v1/partner/streamAuthenticate
Section titled “Authenticate”Send your tenant API key on the upgrade request via the X-API-Key header
(preferred — this is a server-to-server endpoint):
X-API-Key: ps_b2b_…For clients that cannot set a custom header on the WebSocket upgrade, the key may instead be passed as a query parameter:
WS /v1/partner/stream?api_key=ps_b2b_…Only ps_b2b_ tenant keys are accepted. On an auth or gating failure the server
closes the connection with a distinguishable WebSocket close code so your
reconnect logic can react correctly:
| Close code | Meaning | What to do |
|---|---|---|
4401 | Missing, invalid, inactive, or expired key | Fix credentials — don’t retry blindly |
4429 | Tenant over its rate limit or connection cap | Back off, then retry |
4404 | Tenant not yet enabled (the /v1/partner surface is gated) | Contact us; don’t hammer |
# python-websocketstry: async with websockets.connect(url, additional_headers={"X-API-Key": key}) as ws: ...except websockets.ConnectionClosed as e: if e.code == 4429: ... # back off and reconnect elif e.code in (4401, 4404): ... # don't retry — fix the key / ask to be enabledConnect
Section titled “Connect”The first frame the server sends after the upgrade is connected. It carries the
resolved tenant and the wire protocol version — check protocol_version so a
future additive change to the frame schema can’t silently break your parser.
{"type": "connected", "ts": "2026-06-12T10:00:00Z", "tenant": "your-tenant-slug", "protocol_version": "1.0"}Subscribe
Section titled “Subscribe”After the connected frame, send a subscribe op — to a specific set of accounts,
or to every account under your tenant:
{"op": "subscribe", "accounts": ["6f1c…", "9a2e…"]}{"op": "subscribe", "all": true}The server acknowledges with the current subscription set:
{"type": "subscribed", "ts": "2026-06-12T10:00:00Z", "accounts": ["6f1c…", "9a2e…"]}Every account you subscribe to is validated against your tenant. An
account_id that is not yours yields an UNKNOWN_ACCOUNT error frame and is
never streamed — there is no cross-tenant enumeration signal:
{"type": "error", "code": "UNKNOWN_ACCOUNT", "message": "No such account for this tenant.", "account_id": "…"}To stop streaming specific accounts:
{"op": "unsubscribe", "accounts": ["9a2e…"]}Server frames
Section titled “Server frames”Every account-scoped frame carries a common envelope: type, ts (ISO-8601),
account_id, and a per-account monotonic seq (per connection — it resets on
reconnect). All numeric values are strings at a fixed scale — money fields
(equity, balance, upnl, open_value, notional, fee) at 2 decimals,
prices (price, avg_entry_price, mark) and quantities (qty) at 4
decimals. The same scale is used on the REST envelope, so a stream equity
string equals the REST equity string exactly.
snapshot
Section titled “snapshot”Full state — emitted on subscribe, on the periodic drift re-sync, and after a
lifecycle change. This is the canonical shape; the equity frame is a subset.
{ "type": "snapshot", "ts": "2026-06-12T10:00:00Z", "account_id": "6f1c…", "seq": 1, "equity": "10142.42", "balance": "9875.12", "upnl": "142.42", "open_value": "267.30", "positions": [ { "market_id": "0xabc…", "outcome": "Yes", "qty": "10.0000", "avg_entry_price": "0.4000", "mark": "0.5500", "upnl": "1.50", "question": "Will X happen?", "status": "OPEN" } ]}equity=balance+ Σ(open position market value)upnl=equity−starting_balanceopen_value=equity−balancemarkis the current price;nullif no cached price (equity then falls back to the position’s entry price, matching the REST envelope)
equity
Section titled “equity”The 1–2 s tick. Emitted only when equity changed versus the last value sent (after quantizing to cents) — an idle account produces no equity frames.
{"type": "equity", "ts": "…", "account_id": "6f1c…", "seq": 7, "equity": "10150.00", "balance": "10150.00", "upnl": "150.00", "open_value": "0.00"}A new fill, detected within one tick (≤ 2 s) of it landing — including fills written by the background matching daemon.
{"type": "trade", "ts": "…", "account_id": "6f1c…", "seq": 8, "order_id": 101, "client_order_id": "co-101", "side": "BUY", "outcome": "Yes", "market_id": "0xabc…", "qty": "10.0000", "price": "0.4100", "fee": "0.00", "notional": "4.10", "filled_at": "2026-06-12T10:00:01Z"}position_update
Section titled “position_update”Emitted alongside the trade that moved a position, and whenever a qty/status
change is detected. When a fill fully closes a position you receive a
position_update with qty: "0.0000" and status: "CLOSED" (and
avg_entry_price: null) so your position view zeroes cleanly — don’t rely solely
on the trade frame to detect a close.
{"type": "position_update", "ts": "…", "account_id": "6f1c…", "seq": 9, "market_id": "0xabc…", "outcome": "Yes", "qty": "20.0000", "avg_entry_price": "0.4000", "status": "OPEN"}account_lifecycle
Section titled “account_lifecycle”A close, reset, or freeze, detected via the account’s backing wallet status. A
fresh snapshot follows so you see the post-transition state. event is one of
closed, reset, frozen.
{"type": "account_lifecycle", "ts": "…", "account_id": "6f1c…", "seq": 12, "event": "closed"}heartbeat
Section titled “heartbeat”Sent on idle connections (~every 15 s) so intermediaries don’t drop the socket.
{"type": "heartbeat", "ts": "…"}Non-fatal — the connection stays open. Codes: UNKNOWN_ACCOUNT (account not
yours / doesn’t exist), SUBSCRIBE_FAILED (a subscribed account couldn’t be
seeded this attempt — safe to retry the subscribe), ACCOUNT_LIMIT,
RATE_LIMIT, BAD_REQUEST, BAD_JSON, UNKNOWN_OP.
Consuming the stream efficiently
Section titled “Consuming the stream efficiently”The wire format follows the standard snapshot-then-delta model used by most exchange data feeds (Polymarket’s own CLOB WebSocket, Binance user-data streams, Coinbase). A few properties to build against:
- Snapshot is your base state; deltas mutate it. On subscribe (and after a
reconnect or lifecycle event) you get a full
snapshot; thereafter apply the incrementalequity/trade/position_updateframes to your local copy. You never re-request full state on every tick. - Equity frames are change-suppressed. An
equityframe is sent only when the cents-quantized equity actually moved — an idle account is silent (justheartbeats). Don’t treat the absence of an equity frame as “stale”; it means “unchanged.” - Every frame is self-contained and routable. Each carries its own
account_idandtype, so a single connection multiplexing many accounts can be dispatched without per-account connection state. Switch ontype; key your local state onaccount_id. - All monetary values are strings, never JSON numbers. Parse them as exact
decimals — never
float. Equity drives your breach/drawdown logic and must not accumulate binary-float error. The stream’s equity uses the identical decimal math asGET /v1/partner/accounts/{account_id}, so the two never disagree. seqis per-account and monotonic within a connection. Use it to order and de-duplicate; a gap means you missed a frame — recover by reconnecting and taking a fresh snapshot, not by requesting a backfill.- Latency is tick-bounded. Trades and equity surface within one poll cadence (default 1.5 s) of landing. If you need a tighter bound for your risk engine, ask us — the cadence is configurable down to 1 s.
Liveness
Section titled “Liveness”Send a ping at any time; the server replies with a pong.
{"op": "ping"}{"type": "pong", "ts": "…"}Reconnect contract
Section titled “Reconnect contract”If the socket drops, reconnect and resubscribe — you receive a fresh
snapshot per account and the per-account seq restarts at 1. There is no
backfill of frames missed while disconnected; the snapshot is the recovery point.
Re-fetch via GET /v1/partner/accounts/{account_id} if you need a
guaranteed-current value at any moment.
Limits
Section titled “Limits”| Limit | Default |
|---|---|
| Connections per tenant | 5 |
| Accounts per connection | 200 |
| Subscribe ops per second | 1 |
Exceeding the account cap in one subscribe op returns an ACCOUNT_LIMIT error
frame; a second subscribe within one second returns a RATE_LIMIT error frame.
Over-the-cap connection attempts are closed with a 4429 code on the upgrade.