Skip to content

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/stream

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 codeMeaningWhat to do
4401Missing, invalid, inactive, or expired keyFix credentials — don’t retry blindly
4429Tenant over its rate limit or connection capBack off, then retry
4404Tenant not yet enabled (the /v1/partner surface is gated)Contact us; don’t hammer
# python-websockets
try:
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 enabled

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"}

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…"]}

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…"]}

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.

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 = equitystarting_balance
  • open_value = equitybalance
  • mark is the current price; null if no cached price (equity then falls back to the position’s entry price, matching the REST envelope)

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"}

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"}

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"}

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.

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 incremental equity / trade / position_update frames to your local copy. You never re-request full state on every tick.
  • Equity frames are change-suppressed. An equity frame is sent only when the cents-quantized equity actually moved — an idle account is silent (just heartbeats). 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_id and type, so a single connection multiplexing many accounts can be dispatched without per-account connection state. Switch on type; key your local state on account_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 as GET /v1/partner/accounts/{account_id}, so the two never disagree.
  • seq is 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.

Send a ping at any time; the server replies with a pong.

{"op": "ping"}
{"type": "pong", "ts": ""}

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.

LimitDefault
Connections per tenant5
Accounts per connection200
Subscribe ops per second1

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.