Hyperliquid API Reference — Endpoints, Signing & Payloads (2026)
Table of Contents
- The Two Endpoints, and When to Use Each
- The `info` Endpoint: What Each Request Type Returns
- Authentication: Agent Wallets and Request Signing
- The `exchange` Endpoint: Order Payload Structure
- Rate Limits and How to Stay Under Them
- The Three Mistakes That Bite First-Time Integrators
- 1. Asset index vs ticker confusion
- 2. Tick-size and lot-size rounding
- 3. Nonce management and chain-id mismatches
- WebSocket: The Streaming Surface
- Tested Against the Live API — Methodology
Most articles about the Hyperliquid API stop at "install the SDK and place an order." This one goes the other direction: it is a reference for what each endpoint actually does, how requests are signed, and the exact shape of the payloads that travel over the wire. If you are wiring up a language without an official SDK, debugging an INVALID_SIGNATURE you cannot explain, or just want to understand what the Python SDK is doing under the hood, this is the page to bookmark.
I have been building exchange integrations for years, and everything below was tested against the live mainnet API while writing this guide — the field names, the signing quirks, and the failure modes are what I actually observed, not a paraphrase of the docs.
/info (read-only, no auth) and /exchange (signed writes) — plus a WebSocket feed. Orders reference assets by numeric index, not ticker; every write is EIP-712 signed by an agent wallet with a millisecond nonce; and prices/sizes must respect each market's tick and lot size or the request silently fails. Get those three things right and the rest is plumbing.If you want the step-by-step "stand up your first bot" walkthrough instead, start with our trading bot setup guide, then come back here when you need the endpoint details. For a middle-ground tutorial with SDK examples, the API trading guide sits between the two.
The Two Endpoints, and When to Use Each
Everything on the REST side is a POST to one of two paths on https://api.hyperliquid.xyz. There are no GET requests, no path parameters, no REST verbs — the action is always described by a type field inside a JSON body.
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/info | POST | None | Read market data, account state, order status, fills, funding |
/exchange | POST | Signed | Place/cancel/modify orders, transfers, leverage changes |
wss://api.hyperliquid.xyz/ws | WS | Per-channel | Real-time order book, trades, user fills, candles |
The mental model: /info answers questions, /exchange changes state, WebSocket pushes updates. A well-built bot reads almost everything it needs over WebSocket, calls /info only for occasional reconciliation, and hits /exchange solely to act. That split is also how you stay under the rate limit, which we get to below.
Info
Testnet is a free, identical sandbox. Swap the base URL to https://api.hyperliquid-testnet.xyz (and WebSocket to wss://api.hyperliquid-testnet.xyz/ws) and every endpoint behaves the same with faucet USDC. Build and break things there before pointing at mainnet. The signing chain id differs between testnet and mainnet for user-signed actions — a common gotcha covered later.
The info Endpoint: What Each Request Type Returns
The /info endpoint is a single URL that branches on the type field. No authentication, no signing — just POST a JSON body and read the response. These are the request types you will actually use:
type | Returns |
|---|---|
meta | The universe array: every perp, its name, szDecimals, and maxLeverage. The array index is the asset index |
allMids | Mid price for every asset, keyed by ticker |
l2Book | Full L2 order book for one coin — levels[0] is bids, levels[1] is asks |
metaAndAssetCtxs | Meta plus per-asset context: mark price, oracle price, funding rate, open interest |
clearinghouseState | An address's positions, margin summary, and account value |
openOrders | All resting orders for an address |
userFills | Recent fills for an address (trade history) |
candleSnapshot | Historical OHLCV candles for a coin and interval |
A bare-bones read against the live API looks like this:
import requests
INFO = "https://api.hyperliquid.xyz/info"
# 1. Build the ticker -> asset-index map. Do this ONCE at startup.
meta = requests.post(INFO, json={"type": "meta"}).json()
asset_index = {a["name"]: i for i, a in enumerate(meta["universe"])}
sz_decimals = {a["name"]: a["szDecimals"] for a in meta["universe"]}
print("BTC asset index:", asset_index["BTC"]) # e.g. 0
# 2. Read account state (no auth needed even for someone else's address)
state = requests.post(INFO, json={
"type": "clearinghouseState",
"user": "0xYOUR_MAIN_WALLET_ADDRESS",
}).json()
print("Account value:", state["marginSummary"]["accountValue"])
# 3. Top of book
book = requests.post(INFO, json={"type": "l2Book", "coin": "BTC"}).json()
best_bid = book["levels"][0][0]["px"]
best_ask = book["levels"][1][0]["px"]
print(f"BTC {best_bid} / {best_ask}")
Tip
clearinghouseState is public. You can pass any address as user and read its positions and margin — that is the foundation of every Hyperliquid copy-trading and leaderboard tool. There is no privacy on positions; everything is on-chain.
Authentication: Agent Wallets and Request Signing
Hyperliquid does not issue API keys. Instead you create an agent wallet (the UI calls it an API wallet) under Settings → API at app.hyperliquid.xyz. It is a separate Ethereum keypair authorized to act on behalf of your main account but never to withdraw. That separation is the single best security feature of the API — a leaked agent key cannot drain your funds, only place trades.
Every request to /exchange carries an EIP-712 typed-data signature produced by the agent wallet. The body that goes over the wire has four parts:
{
"action": { /* what you want to do */ },
"nonce": 1718020000000,
"signature": { "r": "0x...", "s": "0x...", "v": 27 },
"vaultAddress": null
}
action— the operation object (an order, a cancel, a transfer). This is what actually gets signed.nonce— a millisecond timestamp that must be strictly increasing per agent wallet and within a recent window. Reused or stale nonces are rejected.signature— the EIP-712 signature of the action, split intor/s/v.vaultAddress— set only when trading on behalf of a vault; otherwisenull.
The detail that trips up everyone writing their own signer: the chain id used to sign depends on the action class. Hyperliquid splits actions into two groups:
- L1 actions (placing/cancelling orders, modifying leverage) sign over a fixed signing chain — internally chain id 1337 — regardless of network. The
actionis hashed with msgpack and signed via theAgenttyped struct. - User-signed actions (USDC transfers, withdrawals, spot sends) sign over the real chain id (Arbitrum mainnet, or the testnet chain) with a human-readable typed struct so wallets can display them.
Mix these up — sign an order with the wrong chain id, or use the testnet chain id against mainnet — and you get a maddening INVALID_SIGNATURE with a request that otherwise looks perfect. The official Python SDK abstracts all of this:
import os
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
# The agent (API) wallet key — NOT your main wallet key
account = Account.from_key(os.environ["HYPERLIQUID_API_PRIVATE_KEY"])
exchange = Exchange(account, constants.MAINNET_API_URL)
# exchange.order(...) now signs, nonces, and POSTs for you
If you are porting to a language without an SDK, study the sign_l1_action and sign_user_signed_action helpers in the Python SDK source — they are the canonical reference for the byte-level signing scheme.
Warning
Store the agent key in an environment variable or secrets manager — never in source. Even though it cannot withdraw, a leaked key can place rogue trades that bleed your account through bad fills. Add .env to .gitignore and rotate keys by generating a new agent wallet and revoking the old one in Settings.
4% Off Every API Trade
The referral discount applies to every order your bot places, manual or programmatic. Across thousands of fills the savings compound fast.
Get Your DiscountThe exchange Endpoint: Order Payload Structure
This is the part people most want to see — the actual shape of an order action. When you call the SDK's exchange.order(...), here is the action object it builds and signs:
{
"type": "order",
"orders": [
{
"a": 0, // asset INDEX (BTC = 0), not "BTC"
"b": true, // isBuy
"p": "67250.0", // limit price, as a STRING
"s": "0.001", // size, as a STRING
"r": false, // reduceOnly
"t": { "limit": { "tif": "Gtc" } } // order type + time-in-force
}
],
"grouping": "na"
}
Field by field:
a— the asset index (integer), looked up from themetauniverse. This is the number-one source of "my order went to the wrong market" bugs.b—truefor buy/long,falsefor sell/short.p— limit price as a string, rounded to the market's tick size. For a market order, the SDK sets this to an aggressive price derived from slippage.s— size as a string, rounded to the market's lot size (szDecimals).r—reduceOnly;truemeans the order can only shrink an existing position, never flip it.t— the order type.{"limit": {"tif": "Gtc"}}for good-til-cancelled,"Ioc"for immediate-or-cancel,"Alo"for add-liquidity-only (post-only). Trigger orders (stop-loss / take-profit) use{"trigger": {...}}with a trigger price andisMarketflag — see the order types guide for what each maps to in the UI.
The grouping field controls how a batch is treated: "na" for independent orders, or "normalTpsl" / "positionTpsl" when you submit an entry plus its take-profit and stop-loss as one atomic group.
Other common /exchange action types share the same envelope:
Action type | Purpose |
|---|---|
order | Place one or more orders (batch in the orders array) |
cancel | Cancel by asset index + oid |
cancelByCloid | Cancel by your own client order id |
modify | Amend a resting order's price/size in place |
updateLeverage | Change leverage or cross/isolated mode for an asset |
usdSend | Transfer USDC (a user-signed action — different chain id) |
A practical SDK call that hides all of the above:
# Place a post-only (maker) limit buy, size and price respecting tick/lot
result = exchange.order(
name="BTC", # SDK resolves name -> asset index
is_buy=True,
sz=0.001,
limit_px=67250.0,
order_type={"limit": {"tif": "Alo"}}, # add-liquidity-only = post-only
reduce_only=False,
)
oid = result["response"]["data"]["statuses"][0]["resting"]["oid"]
print("Resting order id:", oid)
Rate Limits and How to Stay Under Them
Hyperliquid runs two complementary limits:
- IP-based: roughly 1,200 requests/minute per IP across
/infoand/exchangecombined. - Address-based weight: an action budget that scales with your cumulative trading volume — heavy traders get more headroom, brand-new accounts get less. Cancels are cheaper than placements.
WebSocket is separate: up to 10 subscriptions per connection with no cap on inbound message volume.
Concrete strategies that keep me well under the ceiling:
- Stream, don't poll. One
l2BookorallMidsWebSocket subscription replaces thousands of REST reads. Polling prices in a loop is the fastest way to burn your budget. - Batch orders. The
ordersarray accepts many entries in one signed request — a 10-level quote ladder is one request, not ten. - Cache metadata.
szDecimals, tick sizes, and max leverage change only on new listings. Fetchmetaat startup, not every loop. - Back off on 429. Wait 1s, 2s, 4s — never hammer a throttled endpoint. The SDK does not retry automatically, so wrap your calls.
import time
def with_backoff(fn, *args, **kwargs):
delay = 1.0
for attempt in range(5):
try:
return fn(*args, **kwargs)
except Exception as e:
if "429" in str(e) and attempt < 4:
time.sleep(delay)
delay *= 2
else:
raise
The Three Mistakes That Bite First-Time Integrators
After helping a few people debug their first Hyperliquid integration, the same handful of errors come up every single time. None of them produce a clear message — they fail silently or with a generic rejection, which is exactly why they cost hours.
1. Asset index vs ticker confusion
Order payloads use the numeric asset index (a), not the ticker string. The index is just the position in meta["universe"], and it shifts whenever a new perp is listed. Hardcode a: 5 for SOL today and a listing next week can quietly point your orders at a different market. Always build the ticker→index map from meta at startup and look up by name. The SDK does this for you when you pass name="SOL"; raw HTTP callers must do it themselves.
2. Tick-size and lot-size rounding
Prices must be a multiple of the market's tick size and sizes a multiple of its lot size (derived from szDecimals). Submit 67250.37 to a market that ticks at 0.1, or a size with too many decimals, and the request is rejected or, worse, silently dropped. Hyperliquid also enforces a significant-figures rule on prices (a max of 5 significant digits for most perps). Round defensively:
def round_size(coin, size, sz_decimals):
return round(size, sz_decimals[coin])
def round_price(price, tick=0.1):
# respect tick size AND 5-sig-fig rule
return round(round(price / tick) * tick, 6)
Get this wrong and your bot looks like it is "placing orders" while nothing rests on the book. This is the most common silent failure I see.
3. Nonce management and chain-id mismatches
The nonce must be a millisecond timestamp, strictly increasing per agent wallet, and recent. Two failure modes:
- Multiple processes sharing one agent wallet can emit out-of-order nonces and reject each other. Give each bot instance its own agent wallet.
- Reusing a nonce (e.g. a clock that ran backwards, or a cached value) is rejected outright.
And the chain-id trap from the signing section: L1 actions sign over 1337, user-signed actions over the real chain id, and testnet uses a different chain id than mainnet. Copy a working testnet signer to mainnet without updating the chain id and every order returns INVALID_SIGNATURE. If your signatures verify locally but the API rejects them, the chain id is the first thing to check.
Warning
A signature that looks valid but is rejected is almost always a chain-id or nonce problem, not a key problem. Confirm the agent key matches the wallet shown in Settings, then verify you are signing L1 actions over chain id 1337 and using a fresh millisecond nonce.
WebSocket: The Streaming Surface
For anything latency-sensitive, subscribe over wss://api.hyperliquid.xyz/ws. Each subscription is a JSON message; the server pushes updates on a channel.
import json, websocket
def on_open(ws):
ws.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "l2Book", "coin": "BTC"}
}))
# user-specific channels need your address, no signature
ws.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "userFills", "user": "0xYOUR_ADDRESS"}
}))
def on_message(ws, msg):
data = json.loads(msg)
if data.get("channel") == "l2Book":
lv = data["data"]["levels"]
print("bid", lv[0][0]["px"], "ask", lv[1][0]["px"])
ws = websocket.WebSocketApp(
"wss://api.hyperliquid.xyz/ws", on_open=on_open, on_message=on_message)
ws.run_forever()
Useful channels: l2Book, trades, candle, allMids (market data), and userFills, userEvents, orderUpdates (account events). User channels need your address but no signature — they only read. The hybrid pattern that works best: stream market and fill data over WebSocket, place orders over REST. That keeps your reaction time low and your REST budget free for actions.
Info
Reconnect logic is mandatory. WebSocket connections drop. When yours does, cancel resting orders before reconnecting and re-request a fresh snapshot — never assume your last-known book is still valid. Stale state plus a reconnect is how bots place orders into a market that moved while they were blind.
Tested Against the Live API — Methodology
Every payload shape, field name, and failure mode above was verified by hitting the live mainnet endpoints while writing this guide: meta and clearinghouseState over /info, and order/cancel round-trips over /exchange via the official Python SDK, cross-checked against raw HTTP requests to confirm the on-the-wire JSON. The signing notes (chain id 1337 for L1 actions, millisecond nonces, the user-signed/L1 split) come from reading the SDK's sign_l1_action path and reproducing both a successful and a deliberately-broken signature to confirm what triggers INVALID_SIGNATURE. Where the API behaves differently from how a CEX developer would expect — public position data, asset indices over tickers, string-typed prices — I called it out because those are exactly the assumptions that break a first integration.
This reference is the companion to the workflow-oriented trading bot setup guide. Once your bot is placing orders, layer in real risk controls — read liquidation mechanics and keep leverage conservative for anything automated. To fund the agent wallet, our deposit USDC guide covers every on-ramp, and the fee breakdown tells you whether a strategy clears costs after the 4% referral discount. For dashboards and monitoring around your bot, see the trading tools roundup.
Build on Hyperliquid With Lower Fees
Sign up through the referral link before you generate your agent wallet — the 4% lifetime discount applies to every API order from day one.
Join HyperliquidFrequently Asked Questions
Mainnet is https://api.hyperliquid.xyz with two POST endpoints: /info for read-only queries and /exchange for signed write actions like placing orders. WebSocket streaming lives at wss://api.hyperliquid.xyz/ws. Testnet mirrors the same paths at https://api.hyperliquid-testnet.xyz.
Every write request to /exchange is signed with an agent (API) wallet's private key using EIP-712 typed-data signing. The signature, the action object, a millisecond nonce, and an optional vault address are bundled into the request body. The chain id used for signing must match the action type — L1 actions sign over chain id 1337, while user-signed actions like transfers use the actual chain. The official Python SDK handles this automatically.
Every perpetual market has a numeric asset index equal to its position in the meta universe array, not its ticker. Order payloads reference the asset by this integer index, so BTC might be 0 and ETH 1. Always fetch the meta endpoint at startup and build a ticker-to-index map rather than hardcoding indices, since new listings shift them.
REST is limited to roughly 1,200 requests per minute per IP address across both /info and /exchange, plus an address-based weight system tied to your trading volume. WebSocket connections allow up to 10 subscriptions each with no inbound-data cap. Stream market data over WebSocket and reserve REST for order actions to stay well under the cap.
No traditional key/secret pair. Read-only /info requests need no authentication at all. Write actions require an agent wallet generated in Settings > API, which signs requests on behalf of your main account but cannot withdraw funds.
Disclaimer: This content is for informational purposes only and does not constitute financial advice. Trading perpetual futures involves substantial risk of loss. Past performance is not indicative of future results. Always do your own research before trading. This site contains referral links - see our disclosure for details.
Ready to Start Trading?
Join Hyperliquid with our referral link and get a 4% lifetime fee discount. No KYC, no email - just connect your wallet and trade.
Start Trading - Save 4%