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

> An independent reference for Lighter's public REST API: base URL, every endpoint we could reach, real response shapes, parameter caps, error codes, and where behavior differs from the docs.

Source: https://lighterpedia.com/guides/trading/lighter-public-api-reference

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

| | |
|---|---|
| Base URL | `https://mainnet.zklighter.elliot.ai/api/v1` |
| Auth | none on any endpoint below |
| Method | `GET` |
| CORS | `Access-Control-Allow-Origin: *` |
| Edge | CloudFront (`X-Cache: Hit from cloudfront`) |
| Verified | September 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:

```json
{"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](/guides/fees/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`.

```json
{"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.

| Parameter | Required | Notes |
|---|---|---|
| `market_id` | yes | integer, from `orderBooks` |
| `limit` | yes | **maximum 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`.

```json
{"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](/privacy/lighter-volume-inflation-zk-verifiability).

### recentTrades

Executed trades for one market, newest first.

```
GET /api/v1/recentTrades?market_id=1&limit=2
```

| Parameter | Required | Notes |
|---|---|---|
| `market_id` | yes | integer |
| `limit` | yes | **maximum 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.

### 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
```

| Parameter | Required | Notes |
|---|---|---|
| `market_id` | yes | integer |
| `resolution` | yes | `1h` verified; an invalid value returns 400 |
| `start_timestamp` | yes | seconds |
| `end_timestamp` | yes | seconds |
| `count_back` | yes | required, 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`.

```json
{"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`.

```json
{"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`.

```json
{"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](/compare/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:

```json
{"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](/contact) 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.**

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.

## FAQ

### What is the base URL for Lighter's public API?

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.

### Does the Lighter API need an API key?

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.

### How many markets does the Lighter API report?

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.

### Is there a rate limit on the Lighter API?

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.

### Can I use the Lighter WebSocket stream?

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.
