l2BookDiff
Stream real-time L2 orderbook diffs (incremental updates) per block.
Every live data message includes both routing identifiers with the same value:
{"type": "l2BookDiff", "channel": "l2BookDiff"}Other payload fields are omitted above. Existing clients may continue routing on either field.
New endpoint - this endpoint is not a part of original Hyperliquid API and is added by us for builder convenience.
Stream incremental L2 orderbook changes as they happen. Each message contains all changed price levels across all subscribed coins for a single block, making this far more bandwidth-efficient than full snapshots.
For full snapshots, use l2Book. For just top-of-book, use bbo.
Subscribe
{
"method": "subscribe",
"subscription": {
"type": "l2BookDiff",
"coins": ["ETH", "BTC"]
}
}Parameters:
coins
string[]
No
Coins to subscribe to. Omit for all markets (requires ws:l2BookDiffAll permission).
marketTypes
string[]
No
All-markets only. Filters delivery by market type β see All-markets filter. Rejected if combined with coins.
All-markets filter
When coins is omitted, the optional marketTypes field restricts the firehose to specific market types. Each entry is "perp", "spot", "outcome", or the wildcard "*" (alone) for "every type the server currently tracks":
Omitting marketTypes defaults to ["perp"] β outcome and spot markets do not appear unless you opt in. The default never grows; new market types must be added to your marketTypes array explicitly. Pass ["*"] to auto-opt-in to future types.
A second subscribe with a different marketTypes value replaces the previous filter rather than coexisting with it.
Unsubscribe
Update data format
Each message contains all changed levels for your subscribed coins in a single block. A message is sent only for blocks that change something you're subscribed to β quiet blocks are skipped, so height is sparse (not contiguous). See Sequencing and gap detection. Levels with sz: "0" indicate that price level has been removed from the book.
Resync signal
When the server detects a block-height gap (e.g., after a service restart, snapshot resync, or stream interruption), it emits a resync message. Discard the affected local book(s) and re-bootstrap from a REST snapshot. A height gap sends one resync per coin for every coin you're subscribed to (not just the coin that gapped) β a gap usually coincides with an epoch change, which invalidates every book.
Also watch epoch yourself β don't rely on the resync message alone. The resync is driven by height-gap detection, and there is a narrow window (right after the server (re)connects to its diff stream) where a new epoch can appear on diffs without a preceding resync. A robust client treats any change in a coin's epoch β with or without a resync message β as "re-bootstrap that coin from a REST snapshot." See Sequencing and gap detection.
Field reference
Envelope fields (on every message):
seq
number
Per-connection message counter β +1 on every message, across all coins. A jump > 1 means you missed a message. Not per-coin, and not related to the per-coin seq inside diffs (see Sequencing and gap detection).
cursor
string
Cursor for session reconnection replay (format: height:timestamp)
Batch data fields (data):
height
number
Block height
time
number
Block timestamp (milliseconds since epoch)
diffs
array
Array of per-coin diffs (only coins with changes in this block)
Per-coin diff fields (each item in diffs):
coin
string
Market symbol (e.g., "ETH", "BTC")
epoch
string
Server epoch (UUID) β changes on service restart
seq
number
Per-coin sequence number β increments by 1 for each diff for this coin
prev_seq
number
Previous per-coin sequence number (for gap detection)
levels
array
Tuple of [bids, asks] β only changed levels are included
levels[][].px
string
Price as decimal string
levels[][].sz
string
Size as decimal string ("0" means level removed)
levels[][].n
number
Number of orders at this level (0 when level removed)
Resync data fields (data when type is "resync"):
type
string
"resync"
coin
string
Market symbol
reason
string
Why resync occurred (e.g., "height_gap")
new_epoch
string
The new server epoch after restart
Sequencing and gap detection
Every l2BookDiff message carries two independent sequence numbers plus a block height. They do different jobs β a robust client tracks both seqs:
Per message β envelope seq (connection health). The top-level seq increments by 1 on every message your subscription receives, across all coins. If it jumps by more than 1, you missed a message on this connection. It is local to your connection (two clients see different values for the same block) and says nothing about any single coin's book.
Per coin β data.diffs[].seq / prev_seq (book integrity). Each coin carries its own seq and prev_seq. For each coin, the next diff's prev_seq must equal the last seq you applied for that coin; a mismatch means you missed a diff for that coin β re-bootstrap it from l2BookDiffSnapshot. This value is global (identical for every client).
The two are not relatable β one envelope step bumps each included coin by 1, but which coins appear varies per block, so you cannot derive one from the other. Track them separately.
height (block position β sparse). data.height is the block height. Messages are sent only for blocks that change something you're subscribed to, so height skips blocks; gaps are normal, not a loss. Never use height + 1 as a completeness check β use the envelope seq for that. height is for aligning a REST snapshot only (discard diffs where height <= snapshot.height).
seq
top-level (envelope)
per connection
yes β +1 per message
a dropped message
seq / prev_seq
data.diffs[]
per coin (global)
yes β per coin
a missed diff for that coin
height
data
block
no β sparse
(snapshot alignment only, not completeness)
Building a local orderbook
Align the snapshot to the stream on height (always present and shared by both), then use the per-coin seq/prev_seq for ongoing gap detection β see Sequencing and gap detection.
Subscribe to
l2BookDifffor your coin(s) β start buffering messages.Fetch snapshot via the REST
l2BookDiffSnapshotendpoint. Noteheight,epoch, and the per-coinseq.Discard already-applied diffs: drop any buffered batch whose
data.height <= snapshot.height.Verify
epoch: every remaining diff'sepochmust equal the snapshot'sepoch. A differentepochmeans the server restarted after the snapshot β re-fetch and restart from step 1.Apply remaining diffs in
heightorder. The first diff for each coin should haveprev_seq == snapshot.seq(the per-coinseq, not the envelopeseq):For each level in
bidsandasks:If
szis"0", remove that price level.Otherwise, upsert the price level with the new
szandn.
Ongoing gap detection: per coin, check
prev_seq == your last applied seq for that coinon each diff; on a mismatch, re-bootstrap that coin. Optionally watch the envelopeseqfor dropped messages.Resync: if you receive a resync message, discard local state and re-bootstrap from step 1.
Examples
Common errors
The subscription feedback and warning shapes are documented in the WebSocket overview. Invalid names are rejected together, for example invalid coin name(s): ["BTC-USD","ETH USD"]. More than 200 names in one request returns too_many_coins with requested and maximum details. A well-formed inactive name succeeds with an inactive_coins warning instead of silently producing no data.
Too many coins- A request may contain at most 200 names; tier limits may be lowerSubscribing to all markets requires permission- Needsws:l2BookDiffAlladd-onRate limit exceeded- Reduce subscription frequencyAuthentication failed- Check API key
Last updated