API reference
Authentication
Create an API key, register the static IP you call from, and exchange the key for an access token.
Every Sahi API request carries a Bearer access token, and you mint your own. There is no OAuth redirect to build and no secret we hold on your behalf: you create an API key in your Sahi account, exchange it for a short-lived token, and send that token on every call.
Three things have to be true before a token will work, and each has a section below. The key must exist with the scopes you need. You must have approved it for the current trading day. And requests that place, modify, or cancel an order must reach us from a registered static IP. The last two are exchange requirements rather than Sahi policy, and both are checked per request rather than once at onboarding.
Paths on this page are relative to two base URLs: the auth base URL, which
serves /api/v1/apikeys/token, and the API base URL, which serves
everything under /v1. If you do not have them, write to
developers@sahi.com.
Create an API key#
Keys are self-serve, from the API keys section of your Sahi account. You are asked to complete two-factor verification first: creating a credential that can place orders is not something a first-factor session can do. The same section is where you approve a key for the day and revoke it.
Give the key a name and choose its scopes.
Scopes are fixed for the life of the key. A token minted from it can carry that set or a subset of it, never more. To trade under a different set, create a different key.
| Scope | Grants |
|---|---|
orders.write |
Place, modify and cancel orders |
orders.read |
Read the order book and trade history |
positions.read |
Read open positions |
holdings.read |
Read demat holdings |
funds.read |
Read available funds and margin |
profile.read |
Read profile details |
marketdata.read |
Read quotes, historical candles and instrument data |
What you get back. A key id, which begins pk_, and a secret, which begins
sahi_sk_. The secret is shown exactly once. Sahi stores only a hash of it
and cannot show it again, so put it straight into your secret store; if you lose
it, revoke the key and create another. The key id is not a secret and is safe to
log.
You may hold at most 5 active keys at a time. Revoked keys do not count against that, but revocation is terminal: a revoked key can never be reactivated.
Revoking is immediate, and it is retrospective. Revoking a key does not merely stop it minting new tokens; every token already minted from it stops being accepted straight away, rather than running out its remaining life. That is the lever to pull if a secret leaks.
Register your static IP#
Placing, modifying, or cancelling an order is permitted only from IP addresses registered in advance. This follows SEBI's framework for retail algorithmic trading, in force since 1 April 2026, and applies to everyone placing orders through the API.
Registration lives with your account, not with a key: one registration covers every key you hold, and adding a key does not mean registering an address again. You register from the same part of the Sahi account UI that manages keys, and, as with keys, it takes a two-factor session.
There are two slots, a primary and a secondary, each holding one address. The second exists for connectivity redundancy: register the egress address of a backup line or a second region, not a second trading process. Both IPv4 and IPv6 are accepted.
Four rules are worth knowing before you register rather than after:
- One address belongs to one account. An IP registered to another Sahi account cannot be registered to yours while that registration stands. Shared office NAT, a shared VPN exit, or a cloud NAT gateway used by more than one trader will collide. There is no family-sharing exception.
- A slot is locked for 7 days after it takes an address. Once a slot holds an IP, that slot will not accept a different one for a week. The two slots are tracked separately, so a locked primary does not stop you setting the secondary. Plan a move rather than discovering the lock during one.
- The lock survives removal. Emptying a slot and re-registering does not reset its clock, and re-adding the address that was just removed is treated like any other change. Removing an address is never itself blocked, though: it only narrows what may trade, so you can always revoke access immediately.
- Exceptional changes go through us. If an address has to move inside the 7-day window, contact support rather than working around it.
What enforcement looks like. The check covers the three order-write
endpoints — POST /orders, PUT /orders/{order_id}, and
DELETE /orders/{order_id}. One of those from an unregistered address is
refused with 403 and the code IP_NOT_ALLOWED, whatever the token would
otherwise have permitted. Everything else — the order book, positions, funds,
holdings, profile, market data, and both WebSocket streams — is not IP-checked,
so a read-only client needs no registration. Removing your last registered
address therefore costs you the ability to trade through the API, not your
access to it.
The address that matters is the public egress address your traffic actually arrives from, which is often not the address configured on the machine. Check it from the host that will run your client before you register.
Approve the key for the trading day#
A key mints tokens only on trading days you have approved it for. Approval is a daily act, done from the same API keys section, and it is an exchange requirement rather than something Sahi can waive.
A trading day runs 06:00 IST to 06:00 IST. Approval lapses at that boundary, so a key approved on Tuesday morning stops minting on Wednesday at 06:00 until it is approved again. Creating a key counts as approving it, so a key is usable on the day it is made without a second step.
An unapproved key still exists and still lists; it simply cannot mint. Attempts
to mint from one are refused with 403.
Mint an access token#
This is the only endpoint your code calls to authenticate, and the only public
one on the auth host. It takes the key id and secret in the body; there is no
Authorization header on this request.
POST /api/v1/apikeys/token
Content-Type: application/json
{
"key_id": "pk_d1jqhcg55l6bd0e0oq3g",
"secret": "sahi_sk_5Kw...Qr_9b1c2d3e",
"scopes": ["marketdata.read"]
}
| Field | Type | Required | Notes |
|---|---|---|---|
key_id |
string | yes | The key id shown when you created the key. |
secret |
string | yes | The secret shown once at creation. |
scopes |
array | no | Narrows the token to a subset of the key's scopes. Omit it to get the key's full set. It can only narrow: asking for a scope the key does not hold is rejected. |
The response is 200 OK:
{
"access_token": "eyJhbGciOiJFUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 65275,
"expires_at": "2026-08-21T06:00:00+05:30",
"scope": "marketdata.read"
}
| Field | Type | Notes |
|---|---|---|
access_token |
string | The Bearer token for every REST and WebSocket call. |
token_type |
string | Always Bearer. |
expires_in |
integer | Seconds until the token expires. |
expires_at |
string | The same moment as an RFC 3339 timestamp: the next 06:00 IST boundary. |
scope |
string | Space-separated scopes the token actually carries. |
Mint one token per trading day and reuse it. The endpoint is rate-limited per
caller and key, and a client that mints on every request will meet that limit;
on 429, back off and retry in the next minute rather than immediately. That
ceiling is the auth host's own: the /v1 endpoints carry separate per-endpoint
budgets, listed under Rate limits.
Narrow the scopes when you can. A process that only reads quotes has no
business holding a token that can place orders, even if the key behind it can.
Passing scopes costs nothing and shrinks what a leaked token is worth.
Call the API#
Send the token on every request to the API host:
GET /v1/positions
Authorization: Bearer eyJhbGciOiJFUzI1NiIs...
One token covers everything it is scoped for, market data included. There is no separate market-data credential.
WebSocket connections authenticate the same way, with the header on the upgrade request. See WebSocket connections.
Token lifetime#
- Tokens expire at 06:00 IST, when the trading day ends. A token lasts to
the end of the cycle it was minted in: 06:00 the next morning for one minted
at 09:00, 06:00 the same morning for one minted at 05:00.
expires_inshrinks as the cycle goes on, so a token minted late in the day is short-lived by design. - There are no refresh tokens. Mint a new one. Daily re-authorisation is the regulatory intent, not an omission, which is also why approval lapses on the same boundary.
- When a token expires, or any
/v1call returns401, mint again. If the new mint returns403, the key needs approving for the new day. - WebSocket connections close with code
4401at expiry or revocation. Reconnect with a fresh token: the order stream then re-runs Stream + Snapshot, and the tick stream re-sends its subscriptions.
When something goes wrong#
The auth host and the API host use the same envelope, {"code", "message"},
with different code vocabularies: the auth host uses PascalCase, and
everything under /v1 uses SCREAMING_SNAKE_CASE. Match on the status code and
the host, not on the shape of the string.
Minting, on the auth host:
| Status | Code | Usually means |
|---|---|---|
400 |
InvalidRequest |
A field is missing or malformed, or the requested scopes are not a subset of the key's |
401 |
Unauthorized |
Unknown key id, wrong secret, or the key has been revoked. These are deliberately indistinguishable |
401 |
Locked |
Your account is frozen or locked |
403 |
Forbidden |
The key is not approved for the current trading day |
429 |
TooManyRequests |
Too many mint attempts. Back off to the next minute |
Calling the API, on the API host:
| Status | Code | Usually means |
|---|---|---|
401 |
UNAUTHORIZED |
The token is missing, malformed, expired, or was revoked with its key |
403 |
IP_NOT_ALLOWED |
An order write did not come from a registered static IP |
403 |
FORBIDDEN |
The token is valid but lacks the scope the endpoint needs |
429 |
RATE_LIMITED |
Over the endpoint's rate limit. Wait out the Retry-After header before retrying |
A 403 IP_NOT_ALLOWED on a call that worked yesterday almost always means your
egress address moved: a new NAT gateway, a failed-over line, a container
rescheduled into another subnet.
Checklist#
- Create a key in your Sahi account with the scopes you need, and store the secret as a backend secret. It is shown once.
- If the client places orders, register the public egress IP it calls from, checked from the host that will run it. Add a secondary only for a genuine backup path.
- Approve the key each trading day.
- Mint a token at start of day from
/api/v1/apikeys/token, narrowingscopesto what that process needs, and reuse it until 06:00 IST. - Download the scrip master at start of day
and map instruments to
instrument_idfor market data, orexchange_segmentplussymbolfor orders. - Call the REST endpoints with the Bearer token, and subscribe to the order-update and tick streams for live updates — applying Stream + Snapshot on every order-stream connect.