Skip to main content
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.
Parameters: nLevels (integer 1–100, default 20).

l2OrderbookUpdates

Initial snapshot followed by event-driven incremental updates with the same shape as l2Orderbook. 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 no id.
The 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.
Each market entry carries the full 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 no id for every market:
Or pass a market ticker as id to receive only that market’s entries:
Both forms are fed from the same publish pipeline: the 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-in throttleMs 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). Its lastSequenceId 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 entry type. Global channel — subscribe with no id.
  • feeTierConfig entries carry the full fee-tier table whenever an operator updates it. On subscribe you get a snapshot with the current table (isSnapshot: true).
  • marketAttributes entries (streaming only) carry per-market Regular Trading Hours and off-hours trading-band events. Every entry names what fired in boundEvent: exitRth / enterRth on an RTH boundary crossing (the exitRth entry also carries the sealed settlement price, sealedVwapSettlementPrice), or expansionZoneEntered, expansionZoneExited, tradingBoundExpanded for a band event. Each carries a timestamp (Unix microseconds) and a per-market marketSequenceNum. Superseded by the dedicated marketAttributes channel — prefer it for RTH / off-hours band state and events.
The tier values above are illustrative, not the current schedule — read the live table from the subscribe snapshot or 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 no id; 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.
Subscribe snapshot — one entry per online market (off-hours market shown):
Streaming update at the RTH open (boundEvent: "enterRth") — the market re-enters regular hours and every band field clears to null:
The equivalent REST read is 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 (no id) 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.
The 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.
The 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:
Streaming 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 the spotPositions channel.
In the snapshot (the 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).
Streaming updates carry one position per frame (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 perp positions stream. The equivalent REST read is GET /v1/spotPositions.
The snapshot (the 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.
Streaming updates carry one changed holding per frame in a 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 a fills 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.
Parameters: 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 own userFills — 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/fills returns up to 1,000 rows per request, newest-first. limit above 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.
Because history is not expired, an outage of any length is fully recoverable: page backwards with 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 with snapshot: 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.
Every frame — snapshot and update — carries a spotFills array (a live frame holds the new fill):

settleLoanResults

Per-account outcomes of POST /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; use GET /v1/orders with an explicit from for older history.
Parameters: 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.
To reconcile without re-subscribing, send a 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:
The snapshot (the subscribed reply) carries two separate arrays:
Keep the two arrays separate in your order state. 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 query GET /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 entry type:
  • leverage entries carry effective leverage per market and that market’s margin mode (marginMode: CROSS or ISOLATED, mirrored by the boolean isolated).
  • feeTier entries carry account-level fee tier state.
  • leverageReject entries (streaming only) report a setLeverage request the engine refused — the REJECTED half of the setLeverage lifecycle, carrying rejectReason (UNDERCOLLATERALIZED, INVALID_LEVERAGE, UNKNOWN_MARKET, or HAS_OPEN_POSITION) and the originating requestId; an applied change arrives as a leverage entry instead.
The snapshot returns one 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. Subscription id is <market>/<timeframe>. Available timeframes: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 1d, 3d, 1w.