Trading operations (
placeOrder, cancelOrder, …) run over the same socket as RPC calls rather than a subscription — see WebSocket → Placing orders.
l2Orderbook
Periodic full snapshots every ~200 ms.nLevels (integer 1–100, default 20).
l2OrderbookUpdates
Initial snapshot followed by event-driven incremental updates with the same shape asl2Orderbook. On each delta, timestamp is the Unix microseconds when the matching engine processed the event that produced it.
trades
Live public trade stream. No snapshot on subscribe.contents is an array of fills from a single taker order match — all entries in one message share the same takerOrderId, timestamp, and sequenceNumber. timestamp is Unix microseconds.
markets
Global channel. Subscribe with noid.
markets map is keyed by marketId (as a string). Every emission is a full snapshot (isSnapshot: true); there are no partial diffs on this channel.
MarketInfo shape (same as REST GET /v1/markets); the fields above are an abridged sample. See the WebSocket section in the sidebar for the complete schema.
oraclePrices
Oracle and mark prices, sourced from Pyth. Each update is a batch of the markets whose price changed in that epoch. Subscribe with noid for every market:
id to receive only that market’s entries:
id filters which entries reach you, not when they are published. Subscribing per market reduces bandwidth, not latency.
epoch and markEpochNanos are Unix nanoseconds; oracleEpoch (the oracle observation time) is Unix microseconds. The subscribe reply (type: subscribed) carries the latest price for every market in scope with epoch: 0 and without the per-row timestamps; later frames are the batches described below. markPrice of "0" means unavailable — do not fall back to price.
Update cadence and measuring staleness
Frames are change-triggered, not periodic. A market is republished only when its oracle price or its mark price has moved since the last publish — an unchanged market may emit nothing at all. Do not rely on a periodic keepalive for any single market. Cadence is therefore a property of market activity, not a rate this channel promises. An actively moving market republishes frequently; a quiet one can go seconds between frames while its price remains current. Upstream Pyth publishing sub-second for an asset does not imply sub-second frames here: if the aggregated price is unchanged, the dedupe gate suppresses it. Measure the cadence you actually receive rather than provisioning against an assumed rate. To cap the rate instead, subscribe with the opt-inthrottleMs field, which merges updates into at most one frame per window.
That distinction makes two separate things worth tracking, and conflating them is the most common source of false staleness alarms:
Sample age at receipt, not on a timer. If you record “time since the last frame’s
epoch” on a fixed-interval metrics tick, you are measuring the inter-frame gap rather than staleness: a quiet market reports a growing apparent age even though every frame it sent was current when it arrived. Stamp the age as each frame arrives, and track the gap as its own separate series.
Two cautions when computing age. Compare against a clock disciplined by NTP — markEpochNanos is stamped server-side, so client clock offset lands directly in the result and can even produce negative ages. And alert against your own measured baseline: the only bound the pipeline enforces is an ingest staleness threshold of 10 s, past which a source price is dropped rather than published, which is far looser than normal operation.
The subscribed acknowledgement carries a full snapshot of current prices with epoch: 0 — a sentinel, not a timestamp. Do not compute an age from the snapshot frame; wait for the first channel_data frame.
bbo
Best bid and offer (top of book). ItslastSequenceId jumps forward over suppressed depth-only updates — see order-book sequence contiguity before gap-checking it. Subscribe with throttleMs to receive at most one frame per window.
exchangeAttributeUpdates
Exchange-wide attribute state, discriminated by entrytype. Global channel — subscribe with no id.
feeTierConfigentries carry the full fee-tier table whenever an operator updates it. On subscribe you get a snapshot with the current table (isSnapshot: true).marketAttributesentries (streaming only) carry per-market Regular Trading Hours and off-hours trading-band events. Every entry names what fired inboundEvent:exitRth/enterRthon an RTH boundary crossing (theexitRthentry also carries the sealed settlement price,sealedVwapSettlementPrice), orexpansionZoneEntered,expansionZoneExited,tradingBoundExpandedfor a band event. Each carries atimestamp(Unix microseconds) and a per-marketmarketSequenceNum. Superseded by the dedicatedmarketAttributeschannel — prefer it for RTH / off-hours band state and events.
GET /v1/feetiers.
The equivalent REST reads are GET /v1/feetiers and GET /v1/markets.
marketAttributes
Per-market Regular Trading Hours (RTH) and off-hours trading-band state for every online market. Global channel — subscribe with noid; all subscribers get the same stream. Supersedes the marketAttributes entry variant on exchangeAttributeUpdates.
Every entry — snapshot and streaming alike — is the market’s complete current attribute block: marketId, marketDisplayName, offHoursInitialMarginFraction, isOutsideRth, currentSettlementPrice, both trading-band edges (upperTradingBound / lowerTradingBound) and their next-level edges (nextUpperTradingBound / nextLowerTradingBound), and per-side expansion-zone state (isUpperInExpansionZone / isLowerInExpansionZone plus the …ZoneEnteredAt / …ExpectedExpansionAt clocks). Fields that don’t currently apply are null, never omitted; field semantics match GET /v1/markets. 24/7 crypto markets are included with isOutsideRth: false and every band field null, and never produce streaming updates.
On subscribe you get one full block per online market (isSnapshot: true) — a complete bootstrap, no REST read needed. Streaming updates (isSnapshot: false) each carry one market’s full post-event block on an RTH boundary crossing (the settlement anchor seals on the off-hours exit) or an off-hours bound event — replace your local entry for that marketId wholesale. Streaming entries add a timestamp (Unix microseconds), a per-market marketSequenceNum, and boundEvent, which is present on every streaming entry: exitRth / enterRth on an RTH boundary crossing, otherwise expansionZoneEntered, expansionZoneExited, or tradingBoundExpanded. boundSide (upper / lower) names the band edge on bound events and is null on RTH crossings and on snapshot entries.
boundEvent: "enterRth") — the market re-enters regular hours and every band field clears to null:
GET /v1/markets.
predictedFunding
Predicted next-hour funding rate for a single market. A new payload is delivered each time the prediction is recomputed:rate1h is the predicted 1-hour rate, sampleCount the number of premium samples behind it, and timestampMs when it was computed (Unix milliseconds).
The initial response on subscribe carries the latest cached prediction; contents is empty when no prediction has been published yet. Unlike most channels, payloads are not wrapped in an isSnapshot envelope.
lendingRates
Global channel (noid) that streams the exchange-wide borrow rate and cumulative borrow index — the lending analog of oraclePrices. One frame per rate segment close (SOFR-driven), carrying the annual borrow rate the new segment opens at (ratePpm, parts-per-million per year: 80000 = 8%), the global borrow index after the close (borrowIndex, a stringified integer at 1e12 scale), and the exchange-wide lending utilization the rate was evaluated at (utilizationPpm, parts-per-million: 1000000 = 100%; null on a tick that predates the field).
A borrower’s live accrued interest = loanBalance × (borrowIndex − the account’s lastBorrowIndex) / 1e12, using the account channel’s lending fields. The index only advances at rate ticks. Settled interest is on REST GET /v1/interest.
subscribed reply replays the last observed tick with isSnapshot: true (empty contents — {} — when no tick has been observed since server start); each rate segment close then arrives as channel_data:
account
Account state snapshot and updates (positions, balances, margin). Re-snapshots every 5 seconds.subscribed reply carries the full snapshot in contents, and the same shape is re-broadcast as channel_data every 5 seconds with isSnapshot: true:
positions is an object keyed by marketId (see positions below) — {} when flat. Field semantics match REST GET /v1/account, but two names differ: accountEquity here is REST’s equity, and lastSequenceId is REST’s sequenceNumber (REST-only pendingDeposits / pendingWithdrawals are not included). accountEquity and freeCollateral are recomputed against live mark prices on every 5-second re-snapshot.
An account the engine has never touched (no deposit or order yet — registering an API key does not count) gets a sentinel snapshot instead of misleading zeros: { "isSnapshot": true, "noActivity": true, "error": "this account has no activity yet", "address": "0x…", "accountIndex": 0 }. The subscription stays open, and real snapshots begin once the account’s first event lands — the same lifecycle state REST reports as 404 on GET /v1/account.
Streaming updates are per-event and carry a different, order-centric shape:
contents.type is one of PLACED, CANCELED, MARGIN_CANCELED, REJECTED, TPSL_PLACED, TPSL_CANCELED, TPSL_TRIGGERED, LIQUIDATION, ADL, BORROWER_LIQUIDATION (the account entering wind-down: orders canceled, collateral frozen against the outstanding claim), and ESTATE_RELEASED (wind-down complete). netQuoteBalance is stamped on every frame; avgFillPrice is present whenever the order has any filled quantity.
Accounts that borrow or hold spot collateral carry a lending section in the snapshot (loanBalance, unsettledInterest, marginState, borrowHeadroom, spotPositions) and lending fields on streaming updates (loanBalance, lastBorrowIndex, inWindDown). All of them are omitted for an account with no loan history and no spot holdings.
positions
Open perpetual positions only — spot collateral holdings stream on thespotPositions channel.
subscribed reply), positions is an object keyed by marketId — stringified integers, e.g. "1" = BTC-USD — not an array. The map has the same shape as positions in REST GET /v1/positions and in the account snapshot; the snapshot does not carry REST’s total item count. An account with no open positions gets "positions": {} (an empty object).
isSnapshot: false): contents holds lastSequenceId and a positions array with a single row, not the keyed object. Each row carries its own sequenceNumber; cumulativeFunding is omitted on streaming rows (read it from the snapshot). A frame that confirms an adjustIsolatedMargin request also carries that request’s requestId. When a position closes it is emitted with side: "FLAT" and zeroed financials.
spotPositions
Spot collateral holdings for a single account, on a dedicated channel — follow collateral without subscribing to the full perppositions stream. The equivalent REST read is GET /v1/spotPositions.
subscribed reply) carries spotPositions as an object keyed by spot asset id (its own namespace, not a perp marketId), omitted when empty. Each entry carries spotAssetId, spotAssetDisplayName (e.g. SPY-USD-SPOT), size (in the asset’s own units), value, and oraclePrice. Like the positions snapshot, it does not carry REST’s total item count.
spotPositions array (isSnapshot: false): the post-event size in asset units ("0" closes the holding — remove it from local state), the event valuation price, and a reason — one of DEPOSIT, WITHDRAWAL, SEIZURE, RELEASE, TRANSFER, UNKNOWN. Merge by spotAssetId.
userFills
Per-user fill stream. Snapshot on subscribe, then live updates. Both wrap their rows in afills array: the snapshot (isSnapshot: true) carries up to nFills rows newest-first, and each streaming frame (isSnapshot: false) carries the new fill. Field semantics match REST GET /v1/fills: price and size are this fill’s execution price and quantity, while originalSize / remainingSize describe the order it belongs to.
nFills (optional) caps how many fills the snapshot includes — default 500, and 500 is also the maximum. A larger value is clamped, not rejected.
Forced closures appear on the closed account’s own stream
Every event that moves your position produces a fill row on your ownuserFills — including a position closed out from under you. If your account is liquidated or auto-deleveraged, you receive your own forced-close leg here and as a row in GET /v1/fills; it is not delivered only to the counterparty. A position can therefore never move without a corresponding fill you can observe, which is what makes the fill stream sufficient on its own to reconstruct net position.
The forced leg carries a liquidation object — method (LIQUIDATION or ADL) and liquidatedUser. Note that liquidatedUser is always the account that owns the row, not a counterparty: on your own row it is your own account. Use the presence of the liquidation object, not its value, to classify a fill as forced. The voluntary side of the same match (a resting maker that got hit) receives an ordinary fill row with no marker. The marker is persisted, so snapshots and REST return it too.
Recovering from a disconnect
The subscribe snapshot and a REST backfill together close any gap, provided you page:- The snapshot replays the most recent 500 fills (
nFills), newest-first. GET /v1/fillsreturns up to 1,000 rows per request, newest-first.limitabove 1,000 is silently clamped rather than rejected.- Fill history is not trimmed to a retention window — there is no TTL on the store. The 1,000 rows are a per-request cap, not the depth available.
from and to (epoch microseconds, matching createdAt): set from to the newest createdAt you already hold and each request’s to to the oldest createdAt you have so far. Omitting from limits a request to the 30 days before to. Deduplicate on tradeId, which is stable across the snapshot and REST views — expect the snapshot and your backfill to overlap rather than abut exactly.
spotFills
Per-user spot fill stream — the seizure and loan-settle executions of the lending system. Spot has no order book, so these are the only spot executions and there is no maker/taker role or fee. Snapshot of recent spot fills on subscribe, then live updates; subscribe withsnapshot: false to skip the snapshot and receive only live fills. Use REST GET /v1/spotFills for history. For settleLoan request outcomes, including rejections (which produce no fill), see settleLoanResults.
spotFills array (a live frame holds the new fill):
settleLoanResults
Per-account outcomes ofPOST /v1/settleLoan requests — one frame per request, APPLIED or REJECTED. Each frame mirrors the REST response body and carries the requestId from the REST 202 ACK, so you can correlate the two. It is the only WebSocket surface for a rejected settlement, which moves no cash and produces no fill: on REJECTED the execution fields read "0", newLoanBalance echoes the unchanged loan, and rejectReason is set. An applied settlement also appears on spotFills as a LOAN_SETTLE fill, but only this channel carries repaidAmount and newLoanBalance. Stream-only: there is no snapshot, the subscribe ack carries empty contents ({}), and frames flow from the moment you subscribe. Optional accountIndex (0–9) selects the subaccount, as on the other account channels.
orders
Open and recently closed orders. The snapshot lists every open order, regardless of age, plus up to 100 orders closed in the last 28 days; useGET /v1/orders with an explicit from for older history.
nRecentClosed (optional) caps how many closed orders the snapshot includes (default 100). Open orders are never capped. market (optional) scopes both snapshot arrays and the live stream to one market — see optional subscribe fields.
get request of type orders on the same socket (envelope and id rules in WebSocket → Message envelopes). Its result carries openOrders / recentClosedOrders arrays of the same shape as the snapshot (recentClosedOrders covers the last 30 days unless you pass from); payload takes address, accountIndex, and optionally market, from, to, limit, and nRecentClosed:
subscribed reply) carries two separate arrays:
recentClosedOrders replays up to nRecentClosed terminal orders on every subscribe — concatenating it with openOrders into a single working-orders list resurrects closed orders as live on every reconnect.
Streaming updates deliver one order event per frame:
createdAt / updatedAt are Unix microseconds.
Streaming status names the event that produced the frame: OPEN (placed, modified in place, or partially filled and still resting), FILLED, CANCELED (including an IOC that filled only partly), MARGIN_CANCELED, REJECTED, TPSL_PLACED, TPSL_TRIGGERED, TPSL_CANCELED, LIQUIDATED and ADL (forced closures). Snapshot rows report the stored status, with the same values as REST GET /v1/orders (OPEN, UNTRIGGERED, FILLED, CANCELED, REJECTED, LIQUIDATED, ADL). Resting orders submitted as GTT read back with timeInForce: "GTC"; treat the two as equivalent.
REJECTED events carry a machine-readable rejectionReason (see the RejectionReason enum); this channel is the source of truth for rejections. A resting order past its goodTilTime is removed only when incoming matching reaches it, and then arrives as REJECTED with rejectionReason: EXPIRED; an order already expired when placed ends CANCELED, and an untriggered TP/SL that expires ends TPSL_CANCELED.
funding
Funding-payment stream for an account. The snapshot on subscribe contains up to 100 of the most recent funding payments from the last 30 days; for older history queryGET /v1/funding with an explicit from. Live updates deliver one event per (market, account) at each funding payment time.
Sign convention on payment: positive = received, negative = paid. time is the Unix-microsecond funding payment time.
accountAttributeUpdates
Per-account attribute state, discriminated by entrytype:
leverageentries carry effective leverage per market and that market’s margin mode (marginMode:CROSSorISOLATED, mirrored by the booleanisolated).feeTierentries carry account-level fee tier state.leverageRejectentries (streaming only) report asetLeveragerequest the engine refused — theREJECTEDhalf of the setLeverage lifecycle, carryingrejectReason(UNDERCOLLATERALIZED,INVALID_LEVERAGE,UNKNOWN_MARKET, orHAS_OPEN_POSITION) and the originatingrequestId; an applied change arrives as aleverageentry instead.
leverage entry per market plus, when available, one feeTier entry; streaming delivers one entry per changed attribute.
accountTransferUpdates
Account transfer events — deposits, withdrawals, and transfers between accounts (type: DEPOSIT, WITHDRAWAL, or INTERNAL_TRANSFER). The snapshot on subscribe contains the most recent events newest-first (isSnapshot: true); each streaming update carries a single event (isSnapshot: false) as it is processed by the engine.
Amounts and balances are human-readable decimal strings in quote currency; globalSequenceId gives strict ordering across all engine events. sourceAccountId / destinationAccountId name where the funds came from and went to — internal account ids (not Ethereum addresses), or the chain on a deposit or withdrawal. The equivalent REST read is GET /v1/accountTransferUpdates.
Every event carries status: APPLIED, or REJECTED with the reason code in rejectReason (for example INSUFFICIENT_COLLATERAL). A rejected operation moved no funds, so its netQuoteBalance and accountSequenceNum are the pre-operation values. Rejections stream only to the account that initiated the operation.
candles
OHLCV candlesticks. The snapshot holds up to 200 closed candles plus the current, still-open candle (so up to 201 entries, newest first), followed by real-time updates. Subscriptionid is <market>/<timeframe>.
Available timeframes: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 1d, 3d, 1w.