WebSocket Protocol
Connect to the AGG WebSocket API to receive live aggregated orderbook updates, public trades, cross-venue arbitrage-return updates, catalog changes, JSON heartbeat events, and authenticated user events across connected venues. Browsers connect with your app ID; backends connect with an API key and can also stream every user’s events.wss://ob.agg.market/ws, which adds an opt-in realtime tier and the
orderbook firehose.
There is no midpoint channel. A midpoint field on an orderbook frame is not a subscription.
- Wire
- SDK
The JSON frames on this page are the protocol.
Getting connected
1
Open a connection
Pass your How the gateway admits the connection depends on where it runs:
appId as a query parameter.- Browser: the handshake’s
Originheader must match one of your app’s allowed origins. Browsers send it automatically. - Server: send your API key in the
x-app-api-keyhandshake header. NoOriginis needed. See Server connections.
Origin is still required.
Any supplied key must be valid; an invalid key closes with 4003 even if the Origin is
allowed. This does not change REST API-key requirements. The app-events channel still
requires an API key issued for your app.To sign a user in, send their token in the authenticate message after
connected. A token query parameter still works but is deprecated, because URLs end up in
proxy and load-balancer logs.2
Wait for the connected message
The server acknowledges the connection with app and optional user context:
auth is browser (admitted by Origin) or key (admitted by x-app-api-key).
protocolVersion 2 adds error codes, request ids, event ids and resume. userId appears
only when a token was passed in the URL. Send subscriptions after this message arrives.3
Subscribe to outcome streams
Send one or more outcome IDs with a channel to start receiving orderbook or trade updates.
Each outcome has its own orderbook — there is no complement derivation.
outcomeId is an outcome’s id (outcomes[].id on REST market responses). Each outcome
belongs to one venue.Keep orderbook and trade subscriptions together at or under 500 per connection, and arb
market subscriptions at or under 500. Ids beyond the limit are rejected with a
subscription_limit error. The other per-connection limits are in Limits.Server connections
Backends authenticate the handshake with an API key from the admin dashboard (Develop → API). The key replaces theOrigin check, and the connection is metered per key
rather than per IP, so many sockets from one server are fine.
WebSocket, pass the ws package to the SDK:
client.createWebSocket(callbacks, { webSocketImpl: WebSocket }).
Orderbook socket
The orderbook channel is also served on a socket of its own:Origin, servers by
x-app-api-key on the handshake, and you wait for connected before subscribing. An API key is
not required for the per-outcome orderbook channel.
orderbook_snapshot and orderbook_delta are the same objects on both sockets. Everything in
Orderbook stream applies. The orderbook socket adds two things:
seq is counted separately on each tier. Do not compare a conflated seq with a realtime seq
for the same outcome. Sending latency on wss://ws.agg.market/ws does not turn on realtime.
In the SDK, set orderbookUrl on createAggClient to move only the orderbook channel to this
socket, and orderbookLatency to pick the tier. See Orderbook streams
for both, and Orderbook firehose for the whole-venue feed.
1. Orderbook stream
Subscribe to an outcome’s orderbook to receive a full snapshot followed by incremental deltas. Each outcome has its own orderbook — there is no complement derivation. Levels carry venue attribution for the outcome’s venue, and the venue’s own book is included alongside. The snapshot and delta objects below do not change on the orderbook socket. That page is whereorderbookUrl moves this channel onto a second socket, and where the optional latency field selects realtime. The default remains the 100 ms conflated tier. A whole-venue feed is the Orderbook firehose.
Subscribe
Initial snapshot
timestamp is Unix milliseconds on the wire, and the SDK reports it as seconds.
If no orderbook is available for an id, you get an error whose message starts with
Snapshot unavailable for <outcomeId> instead of a snapshot. The subscription stays active, so
deltas may still arrive: ignore deltas for an outcome you have no book for. Check that the id is
an outcome id (outcomes[].id), not a market id, then retry with resnapshot after a delay
rather than in a loop.
Incremental deltas
Market resolved
After you subscribe toorderbook for an outcome, a resolution or close arrives once as market_resolved. The outcome’s orderbook and trade subscriptions end with it: nothing further arrives, and you don’t need to unsubscribe. Trade-only subscribers are not sent this message. result is the label of the first outcome with winner: true, or null when none is marked winner. outcomes[] are the sibling outcomes for that market.
status is resolved or closed. outcomes can be empty, and winner is omitted when it isn’t known. payoutPerShare and refundAtCost are omitted when the venue did not report them. winnerSource is venue or matched and is omitted when winner is or when the winner predates this field; a matched winner is provisional (see Inferred winners). Settlement meaning is on Market resolution.
Sequencing and resync
Every orderbook message includes aseq value for that outcome. seq is monotonic within one
stream of the book. Your client is responsible for maintaining local book state and detecting
integrity issues.
Apply deltas when delta.seq === book.seq + 1. After applying, recompute the checksum
(XOR of per-level CRC32 values) and compare it to checksum in the message. If they match, the
book is consistent. The exact level encoding is in the
market-data guide.
Drop stale deltas where delta.seq <= book.seq — these can arrive after a subscribe when
deltas predate the initial snapshot. Silently discard them.
Request a resnapshot if you detect a forward gap (delta.seq > book.seq + 1) or a checksum
mismatch:
orderbook_snapshot, even when its seq is lower than
yours. If AGG rebuilds an outcome’s book, that book starts a new seq stream. When the first
update from the rebuilt book arrives, the gateway sends you a fresh orderbook_snapshot without
being asked. Apply the following deltas against the snapshot’s seq.
Unsubscribe
Staleness checks
Heartbeats prove the connection is alive, not that each outcome is updating. A quiet market can legitimately go several minutes without a delta, so silence alone is not an error, and silence alone can’t tell you a stream has stopped reaching you. We recommend a 5-minute REST watchdog for every client that keeps books open. The gateway can only resend a snapshot when it sees a new update, so a quiet book that went wrong stays wrong. The watchdog confirms each book against the server instead of trusting that the stream is still flowing.- Once a minute, find subscribed outcomes whose book has not changed for 5 minutes.
- Fetch up to 100 of them with
GET /orderbooks/outcomes?venueMarketOutcomeIds=id1,id2,…&depth=1(see Market-data API). Results come back in request order, withnullfor an id that can’t be served. - Compare each returned
seqwith your book’sseq:- Same: the market is quiet and your book is current. Reset its 5-minute timer.
- Different: you missed updates. Send
resnapshotfor that outcome. - Still different on the next check:
unsubscribeandsubscribethe outcome. That resets all per-subscription state on the gateway.
- Ignore an outcome whose book changed while the request was in flight. Its
seqmoved on for a normal reason.
429, wait for Retry-After before the next check.
Best practices
Connection- Wait for
connectedbefore you subscribe. Only the first 20 messages sent before it are kept; the rest are dropped. - Answer protocol pings. Browsers and most WebSocket libraries do this automatically; a connection that misses two pings in a row is closed.
- Treat 45 seconds without any frame, heartbeats included, as a dead connection and reconnect. Heartbeats arrive every 30 seconds.
- Reconnect with exponential backoff and jitter. After reconnecting, discard your books, subscribe again to everything, and rebuild from the new snapshots. Subscriptions don’t survive a new connection.
- Don’t keep reconnecting on
4001or4003. Those mean a missing app id, an inactive or unknown app, a disallowed origin, a missing or revoked API key, or a non-user token, and retrying won’t help until the configuration is fixed.
- Apply a delta only when its
seqis exactly one more than your book’s, and verify itschecksum. - On a forward
seqgap or a checksum mismatch, sendresnapshot. Never keep patching a book you can’t verify. - Replace your book on every
orderbook_snapshot, whatever itsseq. - Run the 5-minute REST watchdog.
- On
market_resolved, drop that outcome’s book.
- Subscribe only to outcomes you are displaying or trading, and unsubscribe when you stop.
- Keep your message handler fast and move heavy work off it. If your client falls more than
about 1 MB behind, the gateway skips frames for it. On orderbooks this shows up as a
seqgap, which a resnapshot repairs. If it stays behind for 10 seconds, the connection is closed with4008. Reconnect and resume to recover user events. Skipped trades, heartbeats, arbitrage updates andmarket_resolvedmessages are not resent.
- Use the
authenticatemessage. ThetokenURL parameter is deprecated. - On
auth_expiring, refresh the token and sendauthenticateagain. After any reconnect, authenticate withresumeFromset to your lasteventId. - If
resumedarrives withcomplete: false, reload orders, balances and withdrawals over REST. - Backends: authenticate the handshake with
x-app-api-keyinstead of setting anOrigin. Send the key only from your backend, never from a browser.
2. Trade stream
Subscribe to the public trade feed for an outcome. No user auth is required.Subscribe
Trade event
tradeId is the venue’s own id for the fill, present when the venue supplies one (Hyperliquid
today). Use it to deduplicate. Two fills can share side, price, size and millisecond timestamp, so
those fields alone cannot identify a trade.
Unsubscribe
3. Authenticated user events
Receive user-specific events, such as order confirmations and balance updates, after the connection has user-level auth.Authenticate
Send the user’s access token afterconnected:
expiresAt is when the token expires (Unix ms). Send authenticate again with a refreshed token
at any time. That re-authenticates without opening a second socket, and it also switches users.
Suspended users are refused. A user can hold at most 20 authenticated sockets at once.
Session lifetime
A user session on the socket lasts only as long as its token:- About a minute before expiry you receive
auth_expiring. Refresh the token and sendauthenticateagain. - At expiry, or within a minute of the user signing out elsewhere or being suspended, you receive
auth_expired. The connection stays open at app level: market data keeps flowing, but user events stop until you authenticate again.
reason is expired, revoked (signed out or token version bumped) or suspended. Clients
created with AggClient.createWebSocket() refresh and re-authenticate for you.
Event ids and resume
Every user event carries aneventId, along with stream: "user" and the userId. Ids increase
within the stream. Keep the last one you processed. When you reconnect, pass it as resumeFrom:
"replayed": true, before live events
continue. Then it sends:
- The replay starts slightly before your cursor and may resend events you already have.
De-duplicate by
eventId. complete: falsemeans some events may be missing. This happens when the cursor is older than 24 hours, more than the last 500 events have passed, or the event log was unavailable. In that case, reload orders, balances and withdrawals over REST.- An event with no
eventIdwas delivered while the event log was unavailable, and a resume can’t replay it.
Order submitted
Balance update
Order event
Venue-agnostic stream of execution-plan lifecycle and terminal order status events. Emitted for every active venue (Polymarket, Kalshi, Predict, …) automatically — no per-venue handling on the client. Discriminate on theevent field.
Step lifecycle (progress UI)
event values: step_started | step_completed | step_waiting | step_failed | step_retrying.
sequence is the 1-based declaration index of the step within the execution plan, and totalSteps is the total step count. Pair them to render progress like “Step 3 of 7”. dag_started also carries totalSteps so the UI can show an empty progress bar before the first step runs. For branching execution plans, sequence reflects declaration order, not strict execution order — parallel branches may emit non-monotonic sequence values.
Terminal fill (success toast + position refresh)
event values: filled | partial_fill. remainingAmountRaw is present only on partial_fill.
Failure (error toast)
event values: failed (order-level) | dag_failed (execution-plan-level).
Execution plan lifecycle (optional)
event values: dag_started | dag_completed | dag_failed | dag_cancelled.
All
order_event messages are broadcast verbatim — the gateway does not filter by event type. Switch on event and ignore types your UI doesn’t care about.redeem_event reports EVM redemption completion. See Market resolution
for how to track claims and update positions.
For a notification-focused recipe, see User Notifications. That page does not repeat these frames.
4. Arbitrage streams
Stream the live cross-venue arbitrage return (arbReturn) for matched markets — the estimated
return, as a decimal fraction (e.g. 0.012 = 1.2%), from the best prices across the venues in a
matched cluster. This is the same value returned by the
List Venue Events API; the stream keeps it live.
There are two ways to consume it: a per-market channel for a focused view (one or a few
markets), and a single coalesced feed channel for a whole listing.
Arbitrage streams are delta-only — there is no snapshot or sequence number. Load the initial
arbReturn from a REST listing, then apply stream updates on top. Updates are last-value-wins
and loss-tolerant: a dropped message self-heals on the next update. Subscribing requires only an
app-level connection (your appId) — arbReturn is not user-scoped. For REST request and
response details, use the API Reference.Per-market channel
Subscribe with one or moremarketIds (a market’s id) to receive an update whenever that
market’s arbitrage return moves.
outcomeIds contains the marketIds you subscribed:
activeVenuesOnly is always true: the value only reflects active venues.
venueEventId is the parent event of the market (or null), so you can roll updates up to an
event-level value (e.g. the max across an event’s markets).
Unsubscribe:
Feed channel
For a listing of many markets, subscribe once to the coalesced feed instead of managing a subscription per visible market. No ids are sent.arbReturn changed since the last flush (deduped to the latest value per market):
5. App events (server-to-server)
A backend connected with an API key can receive every user event of its app on one socket. These are the same frames as in section 3: order submitted, balance update, order, redeem and withdrawal events. Each one carries itsuserId.
- The channel needs an
x-app-api-keyissued for this app on the handshake. Browser connections, and keys from another app, getapp_events_forbidden. eventIdhere is the app stream’s id, not the same as the user stream’s id for the same event. Resume with{ "action": "subscribe", "channel": "app-events", "resumeFrom": "<eventId>" }. The app stream keeps the last 10,000 events or 24 hours, whichever ends first. Theresumedframe works as in Event ids and resume.- Unsubscribe with
{ "action": "unsubscribe", "channel": "app-events" }.
SDK
App events are a fast path, not a ledger. Use webhooks for
delivery you must not miss, and reconcile over REST when a
resumed frame says
complete: false.6. Catalog feed
Stream new, changed and deleted events and markets, so you can keep your own copy of the catalog without polling. The channel is live only: frames sent while you were disconnected are not replayed. Pair it with the REST catch-upGET /catalog/changes, which serves the same items
for the last 24 hours. The whole pattern, and the SDK’s watchCatalog, is in
Catalog Sync.
The channel works on browser and API-key connections. No user token is needed.
Subscribe, optionally narrowed by venue and entity kind:
Confirmation:
invalid_message error and leaves the previous subscription in place.
Each change is one frame carrying the entity’s current state:
event change carries an event snapshot instead (id, venue, externalIdentifier,
title, slug, status, startDate, endDate, createdAt, categories and the same three
matching fields). A delete carries only the id:
- Apply a frame only when its
(version, changedAt)is newer than what you hold for that(entity, id). Frames can repeat, and two quick edits can arrive as one frame. opiscreated,updatedordeleted. On this channelcreatedis best effort and the same entity can be announced ascreatedtwice. Treatcreatedandupdatedas an upsert.clusterAnchorIdcan change as clusters gain and lose members. Key storage onid. See Matching fields.outcomes[].idis the outcome id the orderbook stream takes.- Venues and categories disabled for your app are left out, except deletes.
stream_interrupted that lists catalog, or after any reconnect, frames may be missing:
catch up over GET /catalog/changes from your last applied changedAt minus about a minute.
Unsubscribe:
Connection management
Heartbeat
The service emits a JSON heartbeat event that clients can observe directly:heartbeat (or any frame) as your liveness signal:
if nothing arrives for 45 seconds, reconnect.
A connection that doesn’t answer two pings in a row is closed by the server.
Reconnection
If the connection closes, reconnect with exponential backoff and jitter. Once the new connection sendsconnected, discard your books, subscribe again to everything, and
authenticate with resumeFrom set to your last eventId. A catalog subscriber also re-runs its
catch-up once the new subscription is confirmed:
Error handling
Protocol or validation errors arrive as JSON messages:code, not on message. outcomeIds (or marketIds for arb) lists the ids the error
applies to. id echoes the id you put on the request. Any client frame can carry an id
(string or number), and acks such as subscribed and authenticated echo it too.
An error about a request, such as a rejected subscription, leaves the connection open. An
invalid token also leaves it open: the connection continues at app level.
Connection-level failures send an
error and then close with one of these codes:
Stream interruptions
If the gateway loses its upstream feed briefly, every affected connection receives:orderbook_snapshot frames to follow.
Trades and arb updates published during the gap are not resent. When channels includes
catalog, re-run the catalog catch-up from your last applied changedAt. User events are not affected,
because they don’t use this feed.
Limits
Limits apply per connection unless noted otherwise.resnapshot only works for outcomes the connection is subscribed to, and a resnapshot already in
flight for an outcome absorbs repeat requests for it.
Related
Setup Guide
Provider wiring, auth setup, and SDK client initialization.
Real-Time Orderbook
SDK, hooks, and UI orderbook implementations.
Orderbook streams
Optional second socket, the 100 ms default, and the realtime subscribe field.
Orderbook firehose
Every book on selected venues. Server-only, and off until Snag enables it.
Real-Time Charts
Build live candles from REST history plus WebSocket events.
Real-Time Arbitrage
Live cross-venue arbitrage returns via per-market and feed channels.
User Notifications
Authenticated order and balance events with refresh-aware reconnect logic.
Catalog Sync
Keep your copy of events and markets current with the
catalog channel and REST catch-up.