Python SDK
Authentication
Mint a token in one call or share one across processes, narrow its scopes, and rotate at 06:00 IST.
The SDK signs in the way the API does: it exchanges your API key for an access
token, called minting a token, and sends that token on every call. This page
follows the API reference's Authentication
page step by step and shows how each step looks in Python. Replace pk_...
and sahi_sk_... in the samples with your key id and secret.
Creating a key, registering your IP and approving the key are done in the API Dashboard only; there is no API or SDK call for them.
Create an API key#
Create the key in the API keys section of your Sahi account, with the
scopes your code needs. Copy the secret when it is shown: it is shown once.
The scopes are fixed for the life of the key, and each has a constant, such as
SahiAPI.SCOPE_ORDERS_WRITE; see Create an API key
for what each grants.
Register your static IP#
Order writes (place, modify, cancel) must come from an IP registered on your
account; reads and the streams need none. An order write from any other address
raises SahiIpNotAllowedException. See
Register your static IP
for the rules, including the 7-day lock on a changed address.
Approve the key for the trading day#
A key mints tokens only on a day you have approved it for, in the same API
keys section. Creating a key approves it for that day; approval lapses at
06:00 IST. Minting from an unapproved key raises SahiKeyNotApprovedException.
Mint an access token#
One call: from_credentials#
SahiAPI.from_credentials mints a token and returns a client ready to use.
SahiAPI.from_credentials(key_id: str, secret: str, *, scopes: list[str] | None = None, scrip_master_url: str | None = None, timeout: float = 10.0) -> SahiAPI
from sahiapi import SahiAPI
api = SahiAPI.from_credentials(key_id="pk_...", secret="sahi_sk_...")
| Parameter | Notes |
|---|---|
key_id |
Required. The key id, which begins pk_. Not a secret. |
secret |
Required. The secret shown once when the key was created. |
scopes |
Narrow the token to some of the key's scopes. See Narrow the scopes. |
scrip_master_url |
Another copy of the scrip master. Default: the published file. |
timeout |
Seconds, for the mint and as the client's default. Default 10.0. |
Two steps: share one token#
When several processes should share one token for the day, mint it with
get_access_token and build each client from the result with SahiAPI(token).
get_access_token takes the same arguments as from_credentials, except
scrip_master_url, and returns the endpoint's response unchanged:
access_token, token_type, expires_in, expires_at and scope.
sahiapi.get_access_token(key_id: str, secret: str, *, scopes: list[str] | None = None, timeout: float = 10.0) -> dict
SahiAPI(token: dict | str, *, scrip_master_url: str | None = None, timeout: float = 10.0)
from sahiapi import SahiAPI, get_access_token
token = get_access_token(key_id="pk_...", secret="sahi_sk_...")
api = SahiAPI(token)
- The token is a credential. Until 06:00 IST it can do whatever its scopes allow, placing orders included. Guard it as you would the secret.
- Hand
token["access_token"]to the other processes through your secret store, and build each client there withSahiAPI(access_token). SahiAPI()also takes the bareaccess_tokenstring, and sends no request: the first request is the first call you make.- Mint once per trading day and reuse the token. The mint is rate-limited:
a client that mints on every request gets
SahiRateLimitExceptionand must wait for the next minute.
Narrow the scopes#
A token carries the key's scopes, or the subset you ask for. A process that
only reads quotes should not hold a token that can place orders, even if the
key behind it can. Asking for a scope the key does not hold raises
SahiBadRequestException.
api = SahiAPI.from_credentials(
key_id="pk_...",
secret="sahi_sk_...",
scopes=[SahiAPI.SCOPE_MARKETDATA_READ],
)
print(api.scopes) # frozenset({'marketdata.read'})
Call the API#
The client sends its token on every call; there is nothing more to do. It reports what the token says about itself, read from the token without checking it. This is for information: the server checks every call.
| Property or function | Returns |
|---|---|
api.scopes |
A frozenset of the token's scopes; empty if they cannot be read. |
api.token_expires_at |
When the token expires, as a datetime in UTC, or None if unreadable. |
api.access_token |
The token string in use. Read-only; change it with set_access_token. |
sahiapi.token_scopes(token) |
The same scopes for any token string. Raises SahiConfigurationException if it is not a token. |
sahiapi.token_expires_at(token) |
The same expiry for any token string. Raises SahiConfigurationException if it is not a token. |
Token lifetime#
A token expires at 06:00 IST, when the trading day ends, and there are no
refresh tokens. The client never refreshes on its own: an expired token comes
back as SahiAuthenticationException. At start of day, or when that happens,
mint again and give the client the new token:
api.set_access_token(token: dict | str) -> None
from datetime import datetime, timezone
from sahiapi import get_access_token
if api.token_expires_at and api.token_expires_at < datetime.now(timezone.utc):
api.set_access_token(get_access_token(key_id="pk_...", secret="sahi_sk_..."))
- The client keeps its connections, and every later request uses the new token.
- A running
SahiFeedkeeps the token it was built with. At expiry the server closes its streams; build a new feed after rotating. - Minting again does not cancel earlier tokens; each lasts until 06:00 IST. If a token or the secret leaks, revoke the key: that stops every token minted from it at once.
- A new day's mint raises
SahiKeyNotApprovedExceptionuntil the key is approved for that day.
When something goes wrong#
The same failures as the API's When something goes wrong, by the exception the SDK raises.
Minting (from_credentials, get_access_token):
| Exception | Status, code | Usually means |
|---|---|---|
SahiBadRequestException |
400 InvalidRequest |
A field is missing or malformed, or the scopes asked for are not the key's |
SahiAuthenticationException |
401 Unauthorized |
Unknown key id, wrong secret, or a revoked key. The server does not say which |
SahiAccountLockedException |
401 Locked |
The account is frozen or locked |
SahiKeyNotApprovedException |
403 Forbidden |
The key is not approved for today |
SahiRateLimitException |
429 TooManyRequests |
Too many mints. Wait for the next minute |
Calling the API:
| Exception | Status, code | Usually means |
|---|---|---|
SahiAuthenticationException |
401 UNAUTHORIZED |
The token is missing, expired, or was revoked with its key. Mint a new one |
SahiIpNotAllowedException |
403 IP_NOT_ALLOWED |
An order write did not come from a registered IP |
SahiAuthorisationException |
403 FORBIDDEN |
The token lacks the scope this call needs |
SahiRateLimitException |
429 RATE_LIMITED |
Over the call's rate limit |
A SahiIpNotAllowedException on a call that worked yesterday almost always
means this machine's public address changed. Errors
lists every class.
Checklist#
- Create a key with the scopes you need, and keep the secret out of your code, in an environment variable or a secret store.
- If the client places orders, register the public IP it calls from.
- Approve the key each trading day.
- Mint a token at start of day, narrowed to the scopes that process needs, and reuse it until 06:00 IST.
- Build a new
SahiFeedafter each new token.