API reference
Market data
Last traded price, full quotes with depth, historical candles, and the live tick stream.
Last traded price#
Scope marketdata.read
Single or bulk.
Request#
instrument_id from the scrip master. Repeat the parameter once per instrument, up to 100 per request.Response — 200 OK#
The data object is keyed by instrument_id (string form because JSON object keys are always strings; the value originated as an integer).
Each per-instrument value:
null for non-tradable instruments (e.g. indices).Instruments that don't exist are returned as "data": { "5073182": null }; the request as a whole still returns 200. Validation failures (non-integer i, > 100 entries) return 400 with VALIDATION_FAILED.
Full quote with market depth#
Scope marketdata.read
Single or bulk. Returns the REST equivalent of the tick stream's depth_tick — price, volume, DPR band, open interest, and market depth.
exchange_segment / instrument_type (present on WS ticks) are not repeated here — both are static attributes of the instrument, available from the scrip master the instrument_id came from.
Request#
instrument_id from the scrip master. Repeat the parameter once per instrument, up to 100 per request — the same shape as /v1/marketdata/ltp.Response — 200 OK#
The data object is keyed by instrument_id (string form because JSON object keys are always strings; the value originated as an integer).
Each per-instrument value:
null for non-tradable instruments (e.g. indices).0 for non-tradable instruments.null for non-derivative instruments; always present on derivatives, where 0 is a real value meaning zero open interest — not "unknown".Instruments that don't exist are returned as null (as 5073182 above); the request as a whole still returns 200. Validation failures (non-integer i, > 100 entries) return 400 with VALIDATION_FAILED.
Historical OHLC#
Scope marketdata.read
One instrument per request. Supported size values: 1m, 3m, 5m, 15m, 30m, 1h, 1d. Labels are case-sensitive — 1M is not one minute and is rejected.
Request#
1m, 3m, 5m, 15m, 30m, 1h, 1d.to >= from.false. When true and the market is open, the in-progress candle for the current window is appended as the last entry.false. When true, each candle carries open interest as a 7th array element (see below).Response — 200 OK#
Each candle is a fixed-length array. By default it has 6 elements — [timestamp, open, high, low, close, volume]. When the request sets oi=true, a 7th element (open_interest) is appended to every candle. The width is determined solely by the request flag, so every candle in a given response has the same length.
| Index | Field | Present | Notes |
|---|---|---|---|
| 0 | timestamp |
always | Candle open-time, integer epoch milliseconds, aligned to the window (e.g. for 1m: 09:15:00 IST, 09:16:00 IST, and so on). |
| 1–4 | open, high, low, close |
always | Integer paise. |
| 5 | volume |
always | Quantity traded in the window, in units. 0 for windows with no trades — in that case open, high, low and close all repeat the previous window's close. |
| 6 | open_interest |
only when oi=true |
Integer open interest; 0 for non-derivative instruments. May carry forward across zero-volume windows for derivatives. |
With oi=true each candle is a 7-element array; the appended element is the integer open interest (0 for non-derivative instruments). For example, a futures instrument:
Candles are returned in ascending order of timestamp (index 0). The series covers every window in [from, to] — windows with no trades are included with a 0 volume (index 5) so you do not have to interpolate missing buckets.
A response holds at most 2500 candles per request. When the requested range covers more windows than that, the response carries the most recent 2500 and the older part of the range is omitted — the series is clipped at its start, not its end. has_more is true exactly when that happened.
To walk a longer history, page backward: set to to the first returned timestamp - 1 and re-fetch, repeating until has_more is false. Advancing from does not page — the returned series already ends at to.
An unknown instrument_id returns 200 with an empty candles array. Validation failures (unsupported size, inverted range, non-integer parameters) return 400 with VALIDATION_FAILED.
Option expiries#
Scope marketdata.read
The expiry dates an option chain can be requested for: the unexpired expiries the underlying has listed option contracts on within the scrip master's horizon — 93 days ahead on NSE, 63 on BSE, 123 on MCX and NCOM — nearest first. Longer-dated contracts the exchange lists are not in the scrip master and are not served here. An expiry stays listed through its own expiry day until the F&O session closes.
Request#
instrument_id from the scrip master — the index or stock, not a derivative.Response — 200 OK#
Expiries are calendar dates in IST, YYYY-MM-DD — the one place a date is not an epoch-millisecond timestamp, because an expiry is a trading day, not an instant. An underlying_id with no listed options returns 200 with an empty array. A missing or non-integer underlying_id returns 400 with VALIDATION_FAILED.
Option chain#
Scope marketdata.read
The full option chain for one underlying and expiry: every strike with its call and put leg — price, open interest, volume, top of book, implied volatility and greeks — in one request.
Quote fields are live feed snapshots. Implied volatility and greeks come from Sahi's market-analytics service — the same numbers the Sahi app's option chain shows — and are null for the chains it does not track.
Request#
instrument_id from the scrip master.YYYY-MM-DD, one of the expiries endpoint's values. Omitted, the nearest unexpired expiry is used.Response — 200 OK#
Top level:
YYYY-MM-DD (IST) — useful when the request omitted it.null when no snapshot exists for it.Each strike row:
null when no such contract is listed at this strike or the feed has no snapshot for it yet.Each leg:
null before the contract's first trade.null when the bid book is empty.null when the ask book is empty.12.34 = 12.34%). null when the analytics service does not track this chain, or has not solved this leg — a leg with no premium yet, for instance.implied_volatility is.[0, 1], puts in [-1, 0].last_trade_price / 100).There is no previous_volume field — the live feed does not carry the prior session's volume. Derive it from the candles endpoint (size=1d) if you need it.
An expiry that isn't a listed, unexpired expiry for the underlying — or an underlying_id with no listed options — returns 400 with VALIDATION_FAILED. Malformed parameters (non-integer underlying_id, an expiry not in YYYY-MM-DD form) return 400 with VALIDATION_FAILED. A chain the analytics service does not track still returns 200, with implied_volatility and greeks null on every leg; the feed or the analytics service being unreachable returns 500 with INTERNAL.
Tick stream#
Scope marketdata.read
Three modes select the field subset sent on the wire.
Subscription state is connection-local and is not persisted across disconnects. After any reconnect (4401, 4408, or otherwise) the client must re-send its Subscribe messages — the server starts a new connection with an empty subscription set.
There is nothing to reconcile on this stream. Every tick is a complete snapshot for its instrument at the currently-effective mode, so the newest one you hold is the answer and a duplicate costs nothing — no REST snapshot, no per-key merge. After a reconnect the next tick per instrument brings the value back up to date; if you need one before that tick arrives, read it once from GET /v1/marketdata/ltp or GET /v1/marketdata/quotes.
Wire format#
Binary WebSocket frames. Each frame carries exactly one ClientMessage (client → server) or ServerMessage (server → client), serialised as protobuf wire format.
Schema#
Enums#
| Proto enum | Values (prefix stripped) |
|---|---|
ExchangeSegment |
NSE, BSE, NFO, BFO |
InstrumentType |
INDEX, STOCK, ETF, INDEX_FUT, INDEX_OPT_CE, INDEX_OPT_PE, STOCK_FUT, STOCK_OPT_CE, STOCK_OPT_PE |
Mode |
LTP, QUOTE, DEPTH |
Each value in the schema above is prefixed with its enum name (e.g. EXCHANGE_SEGMENT_NSE); the table strips the prefix for readability.
Client → server (ClientMessage oneof)#
| Variant | Fields | Meaning |
|---|---|---|
subscribe |
mode, instrument_ids |
Add instrument_ids at the given mode. Idempotent. |
unsubscribe |
instrument_ids |
Remove instrument_ids from the connection. |
mode_change |
mode, instrument_ids |
Change mode for already-subscribed instrument_ids. |
Server → client (ServerMessage oneof)#
| Variant | Meaning |
|---|---|
ack |
Server accepted a subscribe / unsubscribe / mode_change. Echoes the action and accepted instrument_ids. |
ltp_tick / quote_tick / depth_tick |
A snapshot for one instrument at the currently-effective mode. |
error |
Non-fatal error (e.g. unknown instrument_id, mode change on an unsubscribed id). Carries code and message; the connection stays open. |
Every tick variant carries the same identity fields (instrument_id, exchange_segment, instrument_type, last_trade_price, last_trade_time). Higher modes add more.
| Variant | Mode | Additional fields beyond the identity set |
|---|---|---|
LtpTick |
LTP |
(none) |
QuoteTick |
QUOTE |
volume_today, dpr_upper, dpr_lower, prev_open_interest, open_interest |
DepthTick |
DEPTH |
QuoteTick fields plus repeated best_buys and best_sells. Each DepthLevel array carries only price levels holding orders, up to 5 or 20 depending on the instrument's feed type (L2 vs L3) — never zero-padded. |
Field semantics#
Price fields (last_trade_price, dpr_upper, dpr_lower, DepthLevel.price) are integer paise. Quantities (volume_today, prev_open_interest, open_interest, DepthLevel.qty, DepthLevel.num_orders) are integer counts. last_trade_time is integer epoch milliseconds.
last_trade_time is absent (proto3 optional, no value) for non-tradable instruments (indices). prev_open_interest and open_interest are absent for non-derivative instruments. volume_today is 0 before the first trade of the day and after the start-of-day reset, then accumulates through the session.
dpr_upper and dpr_lower are the exchange-imposed Daily Price Range bounds (also called circuit limits) in paise — the upper and lower price band within which the exchange will accept orders for this instrument on the current trading day. Orders priced outside this band are rejected by the exchange.
Per-connection limits#
- Maximum subscribed
instrument_idsper connection:1000by default, configurable per account. - Maximum concurrent connections per token:
5. The next is refused with close code4429; see Rate limits. - Mode changes are per-
instrument_id; a connection can hold a mix of modes simultaneously.
Close codes#
| Code | Meaning |
|---|---|
1000 |
Normal closure. |
4401 |
Auth expired. |
4403 |
Forbidden — token lacks marketdata.read. In practice an under-scoped (or otherwise invalid) token is rejected at the HTTP upgrade handshake with 403, before the WebSocket is established; this close code is reserved for scope failures detected after the upgrade. |
4408 |
Ping timeout — no pong received within 15s of the server ping. |
4429 |
Per-token concurrent-connection limit exceeded — 5 open connections. |