# Authentication

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](/api/v1/reference/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](/api/v1/reference/authentication/#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](/api/v1/reference/authentication/#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.

```text
SahiAPI.from_credentials(key_id: str, secret: str, *, scopes: list[str] | None = None, scrip_master_url: str | None = None, timeout: float = 10.0) -> SahiAPI
```

```python
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](#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`.

```text
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)
```

```python
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 with `SahiAPI(access_token)`.
- **`SahiAPI()` also takes the bare `access_token` string**, 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 `SahiRateLimitException` and 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`.

```python
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:

```text
api.set_access_token(token: dict | str) -> None
```

```python
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 `SahiFeed` keeps 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](/api/v1/reference/authentication/#create-an-api-key): that
  stops every token minted from it at once.
- **A new day's mint raises `SahiKeyNotApprovedException`** until the key is
  approved for that day.

## When something goes wrong

The same failures as the API's
[When something goes wrong](/api/v1/reference/authentication/#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](/#rate-limits) |

A `SahiIpNotAllowedException` on a call that worked yesterday almost always
means this machine's public address changed. [Errors](/sdk/python/errors/)
lists every class.

## Checklist

1. Create a key with the scopes you need, and keep the secret out of your
   code, in an environment variable or a secret store.
2. If the client places orders, register the public IP it calls from.
3. Approve the key each trading day.
4. Mint a token at start of day, narrowed to the scopes that process needs, and
   reuse it until 06:00 IST.
5. Build a new `SahiFeed` after each new token.
