Start Trading on Lighter

Lighter Public API Reference: Every Endpoint, Verified by Calling It

By Concept211 (@Concept211)Updated: September 8, 202612 min readLast reviewed: September 2026
Table of Contents

Lighter runs a public, unauthenticated REST API at https://mainnet.zklighter.elliot.ai/api/v1. It is the host this site reads its own market data from, it needs no key, and it will answer a browser fetch from any origin. This page documents every endpoint we could actually reach, with the response shapes as returned rather than as described, and the places where what came back did not match what we expected.

Everything below was read by calling it on September 8, 2026. Nothing here is paraphrased from documentation. Where a call failed, the failure is recorded rather than the endpoint being written up from the outside.

The market-data half of Lighter's API is fully open: no key, no header, Access-Control-Allow-Origin: *, and JSON on every route. The three things that catch people are that orderBooks and orderBookDetails disagree about how many markets exist, that count_back is ignored when you also pass a time range, and that every parameter mistake returns the same opaque error, so a 400 tells you something is wrong but never what.

Base URL and what the host serves

Lighter logoLighter
Base URLhttps://mainnet.zklighter.elliot.ai/api/v1
Authnone on any endpoint below
MethodGET
CORSAccess-Control-Allow-Origin: *
EdgeCloudFront (X-Cache: Hit from cloudfront)
VerifiedSeptember 8, 2026

Two consequences worth knowing before you build against it. Because CORS is open, a front end can call this directly with no proxy. Because CloudFront sits in front, a response you read as live may have been served from an edge cache, so identical repeated requests are not a reliable way to watch something change.

There is one shape rule across the whole API. Every successful body carries "code": 200, and that field is not the HTTP status. A rejected request returns HTTP 400 with a body whose code is 20001. Read the body, not just the status line.

Endpoints

orderBooks

Every market the exchange knows about, with its trading parameters. This is the cheapest way to enumerate markets and the right endpoint for a market list.

GET /api/v1/orderBooks
GET /api/v1/orderBooks?market_id=1

market_id is optional and filters to a single market. It is not obvious from the endpoint name that a filter exists, and it works.

Fields on each entry: symbol, market_id, market_type, base_asset_id, quote_asset_id, status, taker_fee, is_taker_fee_enabled, maker_fee, is_maker_fee_enabled, liquidation_fee, min_base_amount, min_quote_amount, order_quote_limit, supported_size_decimals, supported_price_decimals, supported_quote_decimals, created_at, multiplier.

BTC, as returned:

{"symbol":"BTC","market_id":1,"market_type":"perp","base_asset_id":0,
 "quote_asset_id":0,"status":"active","taker_fee":"0.0000",
 "is_taker_fee_enabled":true,"maker_fee":"0.0000","is_maker_fee_enabled":true,
 "liquidation_fee":"1.0000","min_base_amount":"0.00010",
 "min_quote_amount":"10.000000","order_quote_limit":"281474976.710655",
 "supported_size_decimals":5,"supported_price_decimals":1,
 "supported_quote_decimals":6,"created_at":"1737098461107",
 "multiplier":"1.000000000000000000"}

Note that fees come back as strings, and that "0.0000" on both sides is the standard-account rate we cover in what a zero-fee trade actually costs. created_at is a millisecond epoch delivered as a string.

Read on September 8, 2026: 244 entries. 226 active, 18 inactive. By type, 233 perp and 11 spot. Active only: 216 perp, 10 spot.

orderBookDetails

Everything orderBooks returns, plus live pricing, margin parameters, daily statistics and per-market config. This is the endpoint to use when you want one call and all the numbers.

GET /api/v1/orderBookDetails
GET /api/v1/orderBookDetails?market_id=1

It adds, on top of the orderBooks fields: size_decimals, price_decimals, quote_multiplier, default_initial_margin_fraction, min_initial_margin_fraction, maintenance_margin_fraction, closeout_margin_fraction, mark_price, index_price, last_trade_price, daily_trades_count, daily_base_token_volume, daily_quote_token_volume, daily_price_low, daily_price_high, daily_price_change, open_interest, daily_chart, market_config, strategy_index, market_flags, funding_premium_multiplier, funding_clamp_small, funding_clamp_big, base_interest_rate.

Margin fractions come back as integers rather than decimals, and the scale is not stated anywhere in the response. BTC returned default_initial_margin_fraction: 500, min_initial_margin_fraction: 200, maintenance_margin_fraction: 120, closeout_margin_fraction: 80. daily_chart came back as an empty object {} on every market we looked at, so do not build against it.

The response is split in two. The top level carries order_book_details and a separate spot_order_book_details. Perpetuals are in the first array and spot markets in the second, which is a different arrangement from orderBooks, where both sit in one list. On September 8, 2026 that was 233 perp entries (216 active, 17 inactive) and 11 spot entries (10 active, 1 inactive).

The eleven spot markets, with market ids: ETH/USDC (2048), LIT/USDC (2049), LINK/USDC (2050), UNI/USDC (2051), AAVE/USDC (2052), SKY/USDC (2053), LDO/USDC (2054), AZTEC/USDC (2055, inactive), XAUT/USDC (2056), rhSPY/USDC (2057), rhQQQ/USDC (2058). Spot ids start at 2048; perpetual ids are small integers from 1.

exchangeStats

A compact per-market statistics feed plus exchange-wide totals. Much smaller than orderBookDetails and enough if all you want is last price and daily volume.

GET /api/v1/exchangeStats

Top level: code, total, order_book_stats, daily_usd_volume, daily_trades_count. Each row: symbol, last_trade_price, daily_trades_count, daily_base_token_volume, daily_quote_token_volume, daily_price_change.

{"symbol":"BTC","last_trade_price":78762.8,"daily_trades_count":512026,
 "daily_base_token_volume":5301.05211,
 "daily_quote_token_volume":418436453.67952,
 "daily_price_change":-0.7811220646513608}

total was 244 on September 8, 2026, which is every market including the inactive ones. It is not a count of tradable markets, and reading it as one is the easiest way to overstate the size of the exchange.

Prices here are JSON numbers, while the same values on orderBooks are strings. Both endpoints, same host, different types.

orderBookOrders

The live book for one market: resting asks and bids with their sizes and owners.

GET /api/v1/orderBookOrders?market_id=1&limit=2

Both parameters are required. Omitting limit is an error, not a default.

ParameterRequiredNotes
market_idyesinteger, from orderBooks
limityesmaximum 250, verified by bisection: 250 returns 200, 251 returns 400

Response: code, total_asks, asks, total_bids, bids. Each order carries order_index, order_id, owner_account_index, initial_base_amount, remaining_base_amount, price, order_expiry, transaction_time.

{"order_index":562953294201725,"order_id":"562953294201725",
 "owner_account_index":726714,"initial_base_amount":"2.45409",
 "remaining_base_amount":"2.45409","price":"78765.6",
 "order_expiry":1791281975685,"transaction_time":0}

order_id is the same value as order_index, once as a number and once as a string, which exists because the number exceeds what JavaScript can hold exactly. Use the string. transaction_time came back as 0 on resting orders. order_expiry is a millisecond epoch.

Every resting order is attributed to an owner_account_index. That the book is public down to the account level is part of what the verifiability story on this exchange rests on, which we take apart in what a ZK proof can and cannot verify about reported volume.

recentTrades

Executed trades for one market, newest first.

GET /api/v1/recentTrades?market_id=1&limit=2
ParameterRequiredNotes
market_idyesinteger
limityesmaximum 100; 101 returns 400

Each trade carries a long field list, among them: trade_id and trade_id_str, tx_hash, type, market_id, size, price, usd_amount, ask_id/bid_id with _str variants, ask_client_id/bid_client_id with _str variants, ask_account_id, bid_account_id, is_maker_ask, block_height, timestamp, taker_position_size_before, taker_entry_quote_before, taker_initial_margin_fraction_before, the three matching maker_*_before fields, transaction_time, ask_order_version, bid_order_version.

The *_before fields are the interesting part. Each trade reports the position size, entry quote and margin fraction of both sides before the fill, so you can reconstruct what each account looked like going into it.

is_maker_ask tells you which side was passive. block_height ties the fill to the chain, which is what makes an independent audit of a fill possible at all.

Trade Zero-Fee on Lighter

Signing up with code LIGHTERPEDIA applies the code automatically. Lighter’s referral documentation says new sign-ups are offered one week of Premium with trading fees rebated on the first $10M of volume, and that rebates are discretionary. Standard accounts trade at 0% maker and taker either way.

Start Trading on Lighter

candles

OHLC bars for one market. Note the path is /candles, not /candlesticks, which returns 403.

GET /api/v1/candles?market_id=1&resolution=1h&start_timestamp=1788776325&end_timestamp=1788862725&count_back=2
ParameterRequiredNotes
market_idyesinteger
resolutionyes1h verified; an invalid value returns 400
start_timestampyesseconds
end_timestampyesseconds
count_backyesrequired, and see below

Response keys are single letters: code, r (the echoed resolution), and c (the array). Each bar: t, o, h, l, c, v, V, i.

{"t":1788778800000,"o":79338,"h":79458.3,"l":79237.9,"c":79385.8,
 "v":86.51092000000857,"V":6865389.052282964,"i":29672572773}

t is a millisecond epoch even though the request takes seconds. v is base-token volume and V is quote volume; the two differ only by case. i appears to be a cumulative index and is not documented here because we did not establish what it counts.

count_back did not do anything. We asked for 2 bars across a 24 hour window at 1h and received 24, one per hour of the range. The time range wins. If you want N bars, compute the window.

fundings

Historical funding for one market, same parameter shape as /candles.

GET /api/v1/fundings?market_id=1&resolution=1h&start_timestamp=...&end_timestamp=...&count_back=2

Response: code, resolution, fundings. Each entry: timestamp, value, rate, direction.

{"timestamp":1788778800,"value":"0.95234160","rate":"0.0012","direction":"long"}

Here timestamp is in seconds, while /candles returns milliseconds for the equivalent field. The same request shape, on the same host, returns two different time units. count_back is ignored here too: we asked for 2 and received 24.

funding-rates

The one endpoint that surprised us. Despite the name, it is not Lighter's own funding rates. It is a cross-venue comparison feed.

GET /api/v1/funding-rates

Response: code, funding_rates. Each row: market_id, exchange, symbol, rate.

{"market_id":133,"exchange":"binance","symbol":"BIRB","rate":0.0001}

Read on September 8, 2026: 740 rows across four venues. lighter 216, bybit 195, binance 194, hyperliquid 135. So a single unauthenticated call returns Lighter's funding rate and three competitors' rates for the same symbol, keyed by Lighter's own market_id. If you are building anything that compares venues, this is the endpoint, and the naming gives no hint of it.

We are describing what the field contains, not what to do with it. How the two exchanges differ structurally is a separate question we cover in Lighter vs Hyperliquid.

announcement

Exchange notices, including scheduled downtime.

GET /api/v1/announcement

Returns code and announcements, each with title and content. Useful if you want to know about a network upgrade before your bot finds out by failing.

account and accountsByL1Address

Public account reads. Both return balances and sub-account structure for an index or an Ethereum address.

GET /api/v1/account?by=index&value=1
GET /api/v1/accountsByL1Address?l1_address=0x...

account returns code, total and accounts. accountsByL1Address returns code, l1_address and sub_accounts. Both answered without authentication.

What do the errors mean?

There is one error and you will meet it constantly.

HTTP 400
{"code":20001,"message":"invalid param "}

Every one of these produced exactly that body, byte for byte, trailing space included:

  • a market_id that does not exist (market_id=99999)
  • a required parameter omitted (orderBookOrders with no limit)
  • a value over the cap (limit=500)
  • an invalid enum (resolution=99)

So a 400 tells you a parameter is wrong and nothing else. Change one thing at a time.

A second failure mode looks different and means something else. Unknown or restricted paths return 403 with an empty body: /status, /info, /layer2BasicInfo, /candlesticks and /fundingRates all did. /blocks and /block returned a 403 too, but as a CloudFront HTML error page rather than JSON, so that one is the edge refusing rather than the application. A 403 here does not distinguish "no such endpoint" from "endpoint exists and you may not have it", which makes probing for undocumented routes unproductive.

Is there a rate limit?

We did not find one, and that is a weaker statement than it sounds.

Thirty sequential requests with a cache-busting parameter all returned 200. No response carried any rate-limit header: no X-RateLimit-*, no Retry-After, nothing. Since CloudFront fronts the API and marks responses X-Cache: Hit from cloudfront, requests that look repeated to you may never reach the exchange, so a client-side burst is a poor instrument for measuring an origin limit.

Read that as untested, not as absent. Cache your reads.

Can you use the WebSocket stream?

Not documented here, because we could not open it.

A handshake against wss://mainnet.zklighter.elliot.ai/stream was refused. The upgrade request returned HTTP 400 with this body:

{"code":20558,
 "message":"You are accessing Lighter from a restricted jurisdiction. For more information, see the https://lighter.xyz/terms"}

The same machine, at the same moment, read every REST endpoint on this page from the same host without any restriction. So the geographic gate sits on the stream rather than on the API as a whole.

Writing up subscription formats and message shapes we never received would defeat the purpose of this page, so there is nothing here about them. If you can reach the stream from your jurisdiction and want this section filled in, the contact page is open.

Where does observed behavior differ from the docs?

Collected in one place, each verified on September 8, 2026:

  1. Two endpoints, two market counts. orderBooks returns 244 in one array; orderBookDetails returns 233 perps and 11 spot in two arrays; exchangeStats.total is 244 including inactive markets. Say which endpoint and which status filter produced any number you publish.
  2. count_back is ignored on both /candles and /fundings when a time range is supplied.
  3. Time units are inconsistent. /candles returns t in milliseconds, /fundings returns timestamp in seconds, recentTrades.transaction_time is microseconds, and created_at is a millisecond epoch delivered as a string.
  4. Types are inconsistent. Prices are strings on orderBooks and numbers on exchangeStats.
  5. funding-rates is a multi-venue feed, not Lighter's own rates.
  6. /candlesticks does not exist; /candles does.
  7. orderBooks accepts an undocumented market_id filter.
  8. daily_chart is always empty.
  9. The stream is geo-restricted while REST is not.

Tip

If you only take one thing from this page: pin the endpoint name and the status filter to every market count you quote, and treat total as a count of rows rather than a count of tradable markets.

Method

Every figure and every field list above came from a live unauthenticated GET against https://mainnet.zklighter.elliot.ai/api/v1 on September 8, 2026, read directly from the response body. Parameter caps were established by bisection, not by reading documentation. Endpoints we could not reach are named as unreachable rather than described.

This is an unofficial reference published by an independent third party. It is not written or reviewed by the entities behind Lighter, it can go out of date the moment the exchange ships, and the exchange's own documentation is the authority on anything that matters to you. Nothing here describes authenticated or trading endpoints, and nothing here is advice about what to do with the data.

Cite this page

These figures are free to quote. Copy the citation or the link below.

Plain-text citation

Lighterpedia. "Lighter Public API Reference." Published September 8, 2026, updated September 8, 2026. https://lighterpedia.com/guides/trading/lighter-public-api-reference

HTML link

<a href="https://lighterpedia.com/guides/trading/lighter-public-api-reference">Lighter Public API Reference — Lighterpedia</a>

Methodology and reuse

Every endpoint, field list, parameter cap, market count and error body on this page was read from a live unauthenticated GET against the Lighter public API on September 8, 2026. Parameter caps were found by bisection. Endpoints that could not be reached are recorded as unreachable rather than described. They were read from the Lighter public REST API (mainnet.zklighter.elliot.ai) on and re-verified against the live app. Exchanges change their schedules, so treat any figure older than that date as needing a fresh check — the date above is the one to compare against.

You may republish these figures with attribution and a link to https://lighterpedia.com/guides/trading/lighter-public-api-reference.

Frequently Asked Questions

https://mainnet.zklighter.elliot.ai/api/v1. Every endpoint on this page is a plain GET against that host, needs no API key and no authentication header, and returns JSON. The host also sends Access-Control-Allow-Origin, so a browser can call it directly from another domain.

Not for the market-data endpoints. Order books, market details, exchange stats, recent trades, candles and funding all return data to an unauthenticated GET. Signing and submitting transactions is a separate matter and is not covered here.

It depends which endpoint you ask, which is the easiest mistake to make with this API. On September 8, 2026 orderBooks returned 244 entries of which 226 were status active, while orderBookDetails split the same universe into 233 perpetual entries and 11 spot entries in two separate arrays. exchangeStats reports total 244, counting inactive markets. Quoting a market count without saying which endpoint and which status filter produced it is how the same exchange gets three different numbers.

We did not hit one. Thirty rapid sequential requests all returned 200 and no rate-limit headers were sent on any response. Responses are served through CloudFront and arrive with X-Cache Hit from cloudfront, so repeated identical requests may be answered by the edge rather than reaching the exchange, which makes any limit hard to measure from outside. Treat the absence of an observed limit as untested rather than as permission.

We could not, so this page does not document it. The handshake against wss://mainnet.zklighter.elliot.ai/stream was refused with HTTP 400 and the body code 20558, message 'You are accessing Lighter from a restricted jurisdiction.' The REST endpoints on the same host returned data normally from the same machine at the same time, so the geographic restriction applies to the stream and not to the whole API.

Lighterpedia is an independent, unofficial resource. It is not affiliated with, produced by, reviewed by, endorsed by, or sponsored by Elliot Technologies, Inc., Lighter, Inc., or any of their affiliates. "Lighter" and the Lighter logo are trademarks of their respective owners, used here only to identify the platform this site writes about. For official information, go to lighter.xyz.

Nothing on this site is legal, tax, financial, or investment advice. Trading perpetual futures with leverage can lose you more than you deposit. Rules differ by jurisdiction and change often — check the primary sources and a qualified professional before acting on anything you read here.

This site contains referral links — see the disclosure in the author note above and our full disclaimer.

Ready to Start Trading?

Signing up with code LIGHTERPEDIA applies the code automatically. Lighter’s referral documentation says new sign-ups are offered one week of Premium with trading fees rebated on the first $10M of volume, and that rebates are discretionary. Standard accounts trade at 0% maker and taker either way.

Start Trading on Lighter