For the complete documentation index, see llms.txt. This page is also available as Markdown.

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:

Parameter
Type
Required
Description

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.

Field reference

Envelope fields (on every message):

Field
Type
Description

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):

Field
Type
Description

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):

Field
Type
Description

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"):

Field
Type
Description

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).

Field
Location
Scope
Gapless?
Use it to detect

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.

  1. Subscribe to l2BookDiff for your coin(s) β€” start buffering messages.

  2. Fetch snapshot via the REST l2BookDiffSnapshot endpoint. Note height, epoch, and the per-coin seq.

  3. Discard already-applied diffs: drop any buffered batch whose data.height <= snapshot.height.

  4. Verify epoch: every remaining diff's epoch must equal the snapshot's epoch. A different epoch means the server restarted after the snapshot β€” re-fetch and restart from step 1.

  5. Apply remaining diffs in height order. The first diff for each coin should have prev_seq == snapshot.seq (the per-coin seq, not the envelope seq):

    • For each level in bids and asks:

      • If sz is "0", remove that price level.

      • Otherwise, upsert the price level with the new sz and n.

  6. Ongoing gap detection: per coin, check prev_seq == your last applied seq for that coin on each diff; on a mismatch, re-bootstrap that coin. Optionally watch the envelope seq for dropped messages.

  7. 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.

  1. Too many coins - A request may contain at most 200 names; tier limits may be lower

  2. Subscribing to all markets requires permission - Needs ws:l2BookDiffAll add-on

  3. Rate limit exceeded - Reduce subscription frequency

  4. Authentication failed - Check API key

Last updated