WebSocket: subscriptions, post requests, heartbeats
One WebSocket streams book, trade and account updates as blocks execute and carries signed actions, but drops without warning and forgets every subscription.
You open one connection to wss://api.hyperliquid.xyz/ws and exchange JSON messages. A subscription pushes updates as blocks execute. A poll sees only the state when it lands, and spends IP weight that order placement also needs (one bot’s measured fix).
Say you follow ETH’s book and your own fills, 2 of the 1000 subscriptions an IP may hold. After a drop both are gone: you reconnect, subscribe again, and the fresh userFills snapshot covers the gap.
Subscribing and unsubscribing
Section titled “Subscribing and unsubscribing”{"method": "subscribe", "subscription": {"type": "l2Book", "coin": "ETH"}}The server acknowledges with {"channel": "subscriptionResponse", "data": {...}} echoing the subscription, then sends frames on a channel named after the type ("l2Book"); userEvents arrives on "user". To stop, send "method": "unsubscribe" with an identical subscription object; other subscriptions are unaffected.
Time-series feeds (userFills, userFundings, userTwapSliceFills, userTwapHistory) open with a snapshot of past data tagged isSnapshot: true, then isSnapshot: false. fastAssetCtxs also opens with a snapshot, then sends only changed coins.
Subscription types used in practice
Section titled “Subscription types used in practice”| Type | Parameters | What arrives |
|---|---|---|
l2Book |
coin, optional nSigFigs, mantissa, fast |
full book snapshot, levels: [bids, asks], each level {px, sz, n} |
bbo |
coin |
best bid and offer, sent only when it changes on a block |
trades |
coin |
trades with users: [buyer, seller] and tid |
allMids |
optional dex |
mid prices; spot mids only with the first perp dex |
candle |
coin, interval |
candles; intervals 1m to 1M |
activeAssetCtx |
coin |
mark, mid, volume; funding, open interest and oracle for perps |
orderUpdates |
user |
order objects with a status string |
userFills |
user, optional aggregateByTime |
snapshot, then new fills |
userEvents |
user |
fills, funding, liquidations, non-user cancels, on channel user |
userNonFundingLedgerUpdates |
user |
deposits, withdrawals, transfers, vault flows |
clearinghouseState, openOrders |
user, dex |
one perp dex’s account state or open orders |
spotState |
user, optional isPortfolioMargin |
spot balances |
fastAssetCtxs |
none | base64 raw-deflate JSON of mark and mid prices |
Further types in the docs: notification, webData3, twapStates, activeAssetData (perps only), userFundings, userTwapSliceFills, userTwapHistory, allDexsClearinghouseState, allDexsAssetCtxs, outcomeMetaUpdates. Use the account’s own address for user feeds: an API wallet has no fills or orders of its own.
Book frames are full snapshots
Section titled “Book frames are full snapshots”l2Book frames are not diffs. Each is pushed on a block at least 0.5 after the previous push (the docs omit the unit; seconds by context). Aggregation and depth options are in the numbers.
Posting requests over the socket
Section titled “Posting requests over the socket”Any info request or signed action can go over the socket, wrapping the usual HTTP body:
{ "method": "post", "id": 256, "request": { "type": "action", "payload": { "action": {"type": "order", "orders": [{"a": 4, "b": true, "p": "1100", "s": "0.2", "r": false, "t": {"limit": {"tif": "Gtc"}}}], "grouping": "na"}, "nonce": 1713825891591, "signature": {"r": "0x...", "s": "0x...", "v": 27} } }}The reply arrives on channel post with your id; the HTTP response body is in data.response.payload:
{ "channel": "post", "data": { "id": 256, "response": { "type": "action", "payload": {"status": "ok", "response": {"type": "order", "data": {"statuses": [{"resting": {"oid": 88383}}]}}} } }}- Info requests:
request.typeis"info"and the payload is the info body. - On failure,
response.typeis"error"and the payload is a string mirroring the HTTP status and description. methodandidare mandatory; use a freshidper post to match replies.- Explorer requests are not supported.
Ping within 60 seconds on quiet feeds
Section titled “Ping within 60 seconds on quiet feeds”Send {"method": "ping"} and the server replies {"channel": "pong"}. One production client pings every 30 seconds.
A dropped socket forgets every subscription
Section titled “A dropped socket forgets every subscription”The docs warn that API servers disconnect clients periodically and without announcement. Reconnect with backoff and resend every subscription. Feeds without a snapshot need the matching info request to fill the gap.
Posts are not replayed: an in-flight action may or may not have executed, so reconcile it by cloid with orderStatus or openOrders before resending (orders).
The numbers
Section titled “The numbers”| Limit (per IP) | Value |
|---|---|
| Connections | 10 |
| New connections | 30 per minute |
| Subscriptions | 1000 |
| Unique users across user-specific subscriptions | 10 |
| Messages sent to the server | 2000 per minute, across all connections |
| Post requests in flight | 100, across all connections |
| Idle timeout | 60 s without a message |
l2Book levels |
20 per side, or 5 with fast |
l2Book nSigFigs (significant figures per level) |
2, 3, 4, 5, or null for full precision |
l2Book mantissa |
1, 2 or 5, only with nSigFigs: 5 |
| Candle intervals | 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 1d, 3d, 1w, 1M |
Building a client that survives drops
Section titled “Building a client that survives drops”- One connection per network, with reference-counted subscriptions: components share a feed and the unsubscribe goes out when the last one leaves.
- Key a registry of live subscriptions by their exact JSON: replay it on reconnect, echo it to unsubscribe.
- Track pending post ids with a timeout and keep them below 100. One observed failure is
too many pending post requests id=N. - Following more than 10 users per IP needs polling, or a node’s own output (data).
- Deduplicate fills on
tid, since every resubscribe replays a snapshot andhashis shared by the partial fills of one transaction. For a venue-wide unique trade id the docs use(block_time, coin, tid), becausetidis a 50-bit hash of the buyer’s and seller’s order ids.
Frames that break naive handlers
Section titled “Frames that break naive handlers”activeAssetCtxon a spot or outcome coin replies on channelactiveSpotAssetCtx, withcirculatingSupplyinstead of funding and open interest. A handler keyed only onactiveAssetCtxmisses it.- A dead subscription type returns a frame on channel
error, not silence.webData2subscriptions are dead (the info request still works); the docs’ example still shows awebDatasubscription. outcomeMetaUpdatesframes are arrays; the docs type them as a one-element tuple.fastAssetCtxsis raw DEFLATE with no zlib or gzip header: standard zlib decoding fails, use a raw inflate.- Outcome books report a 0.5 mid when nobody has quoted.
For the two HTTP endpoints beside the socket, see Architecture. For its caps next to REST’s, see Rate limits.