Sahi APIs
Introduction
Build your own trading tools on your Sahi account.
The Sahi APIs let you build your own tools on your Sahi account: read the market, place and manage your orders, and keep track of your positions, holdings and funds.
The REST endpoints are HTTPS with JSON bodies, and there are two WebSocket streams for anything live. Any HTTP client will do, and order updates arrive as JSON; if you would rather not write the HTTP yourself, the Python SDK wraps every endpoint here. The tick stream is the exception: its frames are protobuf, so you compile the schema published with the tick stream for your language before you can decode them, or let the SDK decode them for you.
The endpoints fall into two families:
- Data APIs read the market. Prices, depth, history and the live tick stream, all read-only.
- Trade APIs act on your account: place and manage orders, and read back your positions, holdings, funds and profile.
Every request carries a Bearer access token, and one token covers every scope it was granted — market data included. You mint it yourself from an API key you create in your Sahi account; the key's scopes are fixed at creation and are listed under Create an API key.
Data APIs#
Quotes, candles and ticks, plus the instrument file they are addressed through.
One scope covers all of it, marketdata.read, and none of it is IP-checked.
Instruments are named by instrument_id — the integer id from the scrip
master. Look it up once at start of day and reuse it for every quote, candle and
subscription.
-
Market data
Last traded price, full quotes with depth, historical candles, and the live tick stream.
- Last traded price
- Full quote with market depth
- Historical OHLC
- Option expiries
- Option chain
- Tick stream
-
Scrip master
The daily instrument file every request maps its symbols through.
The tick stream is one of the two WebSocket connections, and the simpler one: every tick is a full snapshot for its instrument, so the newest value wins and there is nothing to reconcile. What both sockets share — authentication, ping and pong, close codes — is under WebSocket connections.
Trade APIs#
Anything that acts on your account, and everything that reports back on it.
Each area has its own scope — orders.write, orders.read, positions.read,
holdings.read, funds.read, profile.read — so a key can be granted exactly
what your own tool does and no more.
Orders name instruments by exchange_segment + symbol rather than by
instrument_id, and placing, modifying or cancelling one must come from a
registered static IP.
Reads are not IP-checked.
Order updates stream over the other WebSocket. That one does need care on connect: a socket that drops replays nothing, so follow Stream + Snapshot to bootstrap without losing events.
-
Orders
Place, modify and cancel orders, read the order book and the trades behind each order, and follow live order updates.
- Place order
- Modify order
- Cancel order
- Order book (today)
- Order details
- Trades for an order
- +1 more
-
Portfolio
Your open positions and demat holdings.
- Positions
- Holdings
-
Funds
Your available balances per order kind, and the margin an order would need before you place it.
- Available balances
- Order margin
- Basket margin
-
Account
Your identity and account status.
- Your profile
How to work through this#
- Authentication comes first. Create an API key, register the static IP you place orders from, and exchange the key for a token. Nothing below works without one.
- Conventions, further down this page. The rules that hold for every endpoint — money in paise, quantities in units, timestamps in milliseconds, identifiers as strings. Reading them once saves debugging later.
- Scrip master. Download the daily
instrument file and map the symbols you care about. Orders address instruments
by
exchange_segmentandsymbol; market data addresses them byinstrument_id, and both come from this file. - The endpoints you need, from the two groups above. Each page lists its endpoints with every field, and shows the request and response beside them.
Getting help#
The endpoint pages are generated from the same contract our own services are built against, so if something here disagrees with what the API does, that is a bug worth reporting. Write to developers@sahi.com.
Conventions#
- Base path:
/v1. Versioning is URL-prefixed. - All bodies are JSON; field names are
snake_case. - Authentication:
Authorization: Bearer <access_token>on every request. See Authentication for how to mint one. - Requests that place, modify, or cancel an order must arrive from a
registered static IP.
One that does not is refused with
403 IP_NOT_ALLOWED, whatever the token permits. Read-only endpoints and the WebSocket streams are not IP-checked. - Timestamps are integer milliseconds since Unix epoch (UTC).
- Money values are integer paise. Example:
2499.55INR is sent and returned as249955. - Quantity is integer units, not lots. Example: 1 NIFTY lot of 65 contracts is
qty: 65. - Instrument identifier:
- Order APIs use
exchange_segment+symbolpair. - Market Data APIs use
instrument_id— the integer value of theidcolumn from the scrip-master CSV. Look it up once at start of day and use it for every market-data and WebSocket call. instrument_idis Sahi's own key, not the exchange token: RELIANCE is12027, while2885is its NSE token and returnsnull.
- Order APIs use
- All identifiers (
order_id,exchange_order_id,user_id) are opaque JSON strings, even when the value looks numeric. Do not parse them as integers — Sahi-issued order IDs can exceed JavaScript's safe-integer range.instrument_idis the exception: it's a JSON integer.
Exchange segment#
| Value | Market | Available on |
|---|---|---|
NSE |
NSE Cash | Order APIs + Market Data |
NFO |
NSE Futures & Options | Order APIs + Market Data |
BSE |
BSE Cash | Order APIs + Market Data |
BFO |
BSE Futures & Options | Order APIs + Market Data |
Enum values#
Error envelope#
Every non-2xx response uses this shape:
{
"code": "VALIDATION_FAILED",
"message": "trigger_price is required for SL-LMT orders"
}
code is a stable enum. The values used across these pages: INVALID_ORDER_STATE, ORDER_NOT_FOUND, VALIDATION_FAILED, FORBIDDEN, NOT_FOUND. UNAUTHORIZED (401) and RATE_LIMITED (429) can come back from any endpoint, IP_NOT_ALLOWED (403) from the order-write endpoints, and NOT_FOUND (404) from an unknown route — or from GET /v1/funds, POST /v1/margins/order, POST /v1/margins/basket and GET /v1/profile when the account behind the token is not yet set up for trading. UNDER_MAINTENANCE (503) comes back from POST /v1/margins/order and POST /v1/margins/basket during the daily 04:45–07:00 IST maintenance window.
Rate limits#
Each endpoint carries its own request budget for your user, over up to three windows. A call over budget is refused with 429 and the standard envelope —
{
"code": "RATE_LIMITED",
"message": "rate limit exceeded"
}
— carrying a Retry-After header in whole seconds. Wait that long: it is what the window that tripped has left to run, so an immediate retry only earns another 429.
| Endpoint | Per second | Per minute | Per day |
|---|---|---|---|
POST /v1/orders, PUT /v1/orders/{order_id}, DELETE /v1/orders/{order_id} |
10 | 250 | 10,000 |
GET /v1/orders, GET /v1/orders/{order_id}, GET /v1/orders/{order_id}/trades |
5 | 250 | 10,000 |
GET /v1/positions |
5 | 250 | 10,000 |
GET /v1/holdings |
5 | 250 | 10,000 |
GET /v1/funds |
1 | — | — |
POST /v1/margins/order, POST /v1/margins/basket |
5 | 250 | 10,000 |
GET /v1/profile |
1 | — | — |
GET /v1/marketdata/ltp |
1 | — | — |
GET /v1/marketdata/quotes |
1 | — | — |
GET /v1/marketdata/candles |
2 | 250 | 100,000 |
GET /v1/marketdata/optionchain |
2 | 120 | 20,000 |
GET /v1/marketdata/optionchain/expiries |
5 | 250 | 10,000 |
WSS /v1/stream/orders |
5 | 250 | 10,000 |
WSS /v1/stream/marketdata |
5 | 250 | 10,000 |
- Every row is its own counter. Rows that share numbers do not share a budget: polling the order book never eats into your market-data allowance. The exceptions are the rows that list several endpoints, and they are charged together: place, modify and cancel are one surface onto the exchange, the three order reads are one view of the same book, and the two margin checks are one calculator.
—means that window is not enforced for the endpoint; only the ceilings shown apply.- Windows are charged finest first, and a refusal stops there. A call the per-second ceiling rejects never spends minute or day quota, so a burst cannot drain a daily budget in rejections alone.
- The day window rolls from your first request in it, not from midnight or the start of the trading day.
- The WebSocket rows charge the upgrade request, not the frames. Once a socket is open, what flows over it is uncounted; a reconnect loop is counted.
Batch before you back off. ltp and quotes take up to 100 instrument_ids per request, so one call a second is a whole watchlist a second — the ceiling bites only if you loop an instrument at a time. Anything you follow continuously belongs on the tick stream rather than in a polling loop, which is also why the streams are budgeted for reconnects rather than for traffic.
Concurrent connections. Each stream allows 5 open at once — per account for order updates, per token for the tick stream — and refuses the next with close code 4429.
If a strategy genuinely needs more than a row allows, write to developers@sahi.com rather than sharding across keys to work around it.
WebSocket connections#
Two streams, each documented with the resource it carries: order updates is JSON-framed text, and the tick stream is protobuf-framed binary. Four rules hold for both.
- Authentication is the same Bearer JWT as REST, sent in the upgrade request's
Authorizationheader. There is no in-band login frame. - On token expiry the server closes with
4401. Mint a fresh token and reconnect; a socket is never extended past its token'sexp. - Connection health. The server pings every 30 seconds and expects a pong within 15 seconds. If none arrives it closes with
4408. Most WebSocket libraries answer pings for you; if you work with raw frames, send the pong yourself. - Nothing is replayed for the time a socket was down, and no connection state survives a reconnect. What that costs you differs by stream, so the recovery step lives with each: order updates re-run Stream + Snapshot, and the tick stream re-sends its subscriptions.