SAHI DOCS

Python SDK

Errors

The exception hierarchy, how a reply maps onto it, what each exception carries, and how to catch by class.

Every error the SDK raises descends from SahiBaseException, so one except clause can handle anything it might raise. Below that the tree splits three ways, by where the problem is:

Catch by class, not by code or message. The auth host (apis.sahi.com) and the API host (direct.sahi.com) spell the same code differently (Unauthorized, UNAUTHORIZED); the class is the same for both.

The SDK also raises three plain Python exceptions, before sending anything:

The tree#

SahiKeyNotApprovedException is a subclass of SahiAuthorisationException, so an except SahiAuthorisationException around a mint and a call catches both: catch SahiKeyNotApprovedException first if you handle them differently.

How a reply becomes an exception#

A known code decides the class whatever the status; otherwise the status and the host do. Anything not in the table becomes SahiServerException for a 5xx and SahiAPIException otherwise. A 4xx without the API's error envelope was refused by the edge before it reached the API, usually for an over-long URL, and is a plain SahiAPIException.

Status Host Code Class
any any VALIDATION_FAILED SahiValidationException
any any ORDER_NOT_FOUND SahiOrderNotFoundException
any any INVALID_ORDER_STATE SahiInvalidOrderStateException
any any IP_NOT_ALLOWED SahiIpNotAllowedException
any any Locked SahiAccountLockedException
any any UNDER_MAINTENANCE SahiMaintenanceException
4xx edge no envelope SahiAPIException
400 any other SahiBadRequestException
401 any other SahiAuthenticationException
403 auth other SahiKeyNotApprovedException
403 API other SahiAuthorisationException
404 any other SahiNotFoundException
429 any any SahiRateLimitException
5xx any any SahiServerException

What an API exception carries#

Attribute Type Meaning
status int or None The HTTP status, or None when no reply arrived.
code str or None The envelope's code, verbatim as the host sent it.
message str or None The server's message.
request_id str or None The id to quote to Sahi support: the server's when it returned one, otherwise the one the SDK sent.
hint str What to do about it, per class.

str(exc) combines all of them, so logging the exception is enough to diagnose and act on it:

HTTP 403 IP_NOT_ALLOWED: order writes must come from a registered IP. Order writes must come from a static IP registered on your account. Check the public egress address of this host — it usually means the address changed. [request_id=sahiapi-5f1c...]

SahiRateLimitException carries one thing more: retry_after, seconds to wait, from the Retry-After header, or None on the auth host, which sends none; wait for the next minute there.

A feed exception carries close_code instead: the WebSocket close code the server sent (4401, 4403 or 4429; the SahiAPI.WS_CLOSE_* constants name them), or None when there was none, such as a refused handshake.

Retries#

The SDK retries one case itself: a GET that gets 429 with a Retry-After of five seconds or less is retried once, after that wait. Everything else is raised for you to handle:

Catching by class#

Catch the class you can act on and let the rest propagate. Around the mint:

from sahiapi import SahiAPI, SahiAuthenticationException, SahiKeyNotApprovedException

try:
    api = SahiAPI.from_credentials(key_id="pk_...", secret="sahi_sk_...")
except SahiKeyNotApprovedException:
    print("approve the key for today in your Sahi account, then run again")
    raise
except SahiAuthenticationException as e:
    print("sign-in failed:", e)  # wrong secret, revoked key, locked account
    raise

Around an order write, wrap the place_order call of Getting started's Place and cancel an order like this:

from sahiapi import (
    SahiIpNotAllowedException,
    SahiRateLimitException,
    SahiTimeoutException,
    SahiValidationException,
)

try:
    result = api.place_order(...)  # the same arguments as in Getting started
except SahiIpNotAllowedException:
    print("this machine's public IP is not registered on the account")
except SahiValidationException as e:
    print("refused:", e.message)
except SahiRateLimitException as e:
    print("over the limit; wait", e.retry_after, "seconds")
except SahiTimeoutException:
    print("it may have gone through; check api.get_order_list() before resending")

For a stream, the exception comes out of consume():

from sahiapi import SahiFeed, SahiFeedAuthExpiredException

feed = SahiFeed(api)
feed.subscribe_ltp([instrument_id], on_data_received=lambda data, meta: print(data))
try:
    feed.consume()
except SahiFeedAuthExpiredException:
    print("the token expired at 06:00 IST; mint a new one and build a new feed")

Every class#

SahiBaseException#

Base class for everything the SDK raises.

SahiConfigurationException#

The SDK was misconfigured or misused before a request could be made: a SahiAPI() or set_access_token() given something other than the token response or its access_token string, a value passed to sahiapi.token_expires_at() or sahiapi.token_scopes() that is not a JWT, or a modify_order with nothing to change. The message says what was wrong and what to pass instead.

SahiAPIException#

A request to a Sahi host failed. Usually a non-2xx reply; the two subclasses SahiConnectionException and SahiTimeoutException cover the cases where no reply arrived. Also raised directly for a failed scrip-master download, with status set when the file host answered.

SahiBadRequestException#

400: the request was malformed. On the auth host this also covers scopes that are not a subset of the key's.

SahiValidationException#

VALIDATION_FAILED, as 400 for a malformed field or 422 for one the API rejects on meaning: an unknown symbol, a quantity over the freeze quantity, a contract past expiry, a REGULAR order while the market is closed. The server message names the field or rule.

SahiAuthenticationException#

401: the credential or token was missing, unknown, wrong, revoked or expired. The auth host deliberately does not say which. When a /v1 call raises this, mint a new token.

SahiAccountLockedException#

401 Locked: the account behind the credential is frozen or locked. Contact Sahi support.

SahiAuthorisationException#

403 under /v1: the token is valid but lacks the scope the endpoint needs. Scopes are fixed when the key is created and a token can only narrow them.

SahiKeyNotApprovedException#

403 on the auth host: the key is not approved for the current trading day. Approve it in your Sahi account and mint again.

SahiIpNotAllowedException#

403 IP_NOT_ALLOWED: an order write did not come from a registered IP. Reads and market data are not IP-checked. It usually means this machine's public address changed.

SahiNotFoundException#

404: the route or resource does not exist. Also returned by the funds, margin and profile endpoints when the account behind the token is not yet set up for trading.

SahiOrderNotFoundException#

404 ORDER_NOT_FOUND: no such order on today's book for this account.

SahiInvalidOrderStateException#

409 INVALID_ORDER_STATE: only Open and OpenPartial orders can be modified or cancelled; a completed, cancelled or rejected order is final.

SahiRateLimitException#

429: a rate-limit window was exhausted. retry_after is the seconds the window has left, or None on the auth host. Batch requests rather than looping per instrument.

SahiServerException#

5xx: a failure on Sahi's side, usually code INTERNAL. Retry a read after a short wait; check the order book before retrying an order write. If a read keeps failing, write to developers@sahi.com with the request_id.

SahiMaintenanceException#

503 UNDER_MAINTENANCE: the margin checks are closed for the daily 04:45–07:00 IST start-of-day processing. A subclass of SahiServerException. Retry after 07:00 IST.

SahiTimeoutException#

No reply arrived within the timeout. The request may or may not have reached the server. For an order write, look for the order before resending it: Find the order after a timeout.

SahiConnectionException#

The connection to the host could not be opened: DNS, connection refused, a TLS error, a connect timeout. Nothing was sent, so the request is safe to retry once the network is fixed.

SahiFeedException#

Base class for stream errors; carries close_code.

SahiFeedConnectionException#

A connection-level refusal: close code 4429 when the account or token already has five connections open, which clears once another closes; or close_code None for a send attempted while the stream is not connected. A heartbeat drop, a slow-consumer drop or a network drop is not this exception: those reconnect on their own.

SahiFeedAuthExpiredException#

The token was rejected: close code 4401, or 401 at the handshake. Mint a fresh token, build a new feed and subscribe again.

SahiFeedForbiddenException#

The token lacks the stream's scope: 403 at the handshake, or close 4403.

SahiFeedNotSubscribedException#

A cached value was requested for an instrument that is not subscribed.