> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lms.bsa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Authorization

> How to obtain an auth token and how the API decides what you are allowed to do.

This page covers the two halves of access control:

1. **Getting a token** — proving who you are with a username/password and
   receiving a JWT.
2. **Authorization** — what that token lets you do, and which rules
   each endpoint is gated by.

Once you have a token, every subsequent request uses it as documented
in [Authentication](/concepts/authentication).

## Before you start

You need a **partner account** — an email + password issued by the
Blackswan integration team. If you don't have one yet, contact
[hello@bsa.ai](mailto:hello@bsa.ai). Accounts come with the **admin**
role attached, which is what every partner-facing endpoint requires.

Token issuance lives on a separate **auth host** from the main API:

| Purpose                                              | Host                         |
| ---------------------------------------------------- | ---------------------------- |
| Token issuance (`/v1/auth/token`)                    | `https://auth-api.bsa.ai`    |
| Everything else (`/v1/customers`, `/v1/loans`, etc.) | `https://api-staging.bsa.ai` |

The same partner credentials are used to mint a token on the auth host,
and the resulting JWT is presented as a Bearer token on the main API.

## 1. Obtain a token

Call `GET /v1/auth/token` with HTTP **Basic** authentication. The
endpoint returns a signed JWT.

### Headers

```http theme={null}
Authorization: Basic <base64(email:password)>
```

Most HTTP clients do this for you — `curl -u email:password` or
`requests.get(..., auth=(email, password))`.

### Example

```bash theme={null}
curl -sf "https://auth-api.bsa.ai/v1/auth/token" \
  -u "ada@example.com:correct-horse-battery-staple"
```

### Response

```json theme={null}
{
  "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjU0YmIyMTY1...XYZ"
}
```

The header carries the `kid` of the key that signed it (so callers
relying on JWKS verification get the right key), and the payload
carries the standard claims:

```json theme={null}
{
  "iss": "service-project",
  "sub": "5cf37266-3473-4006-984f-9325122678b7",
  "iat": 1779659075,
  "exp": 1811195075,
  "roles": ["ADMIN"]
}
```

### Token lifetime

Tokens are valid for **365 days** from issuance (`exp - iat = 31_536_000`).
There's no refresh endpoint — when expiry approaches, just call
`GET /v1/auth/token` again with your Basic credentials.

### Handling expiry

Pick whichever pattern fits your architecture; they're not mutually
exclusive.

#### Proactive — decode the `exp` claim

The cleanest approach if you cache the token. JWTs are
`header.payload.signature` with each segment as base64url-encoded JSON.
Decode the payload, read `exp` (unix seconds), refresh when the
remaining lifetime drops below a threshold (e.g. 7 days).

```python theme={null}
import base64, json, time, requests

CACHE = {}  # in-memory; replace with your real cache layer
REFRESH_BEFORE = 7 * 86400  # 7 days

def get_token():
    tok = CACHE.get("token")
    if tok and _seconds_remaining(tok) > REFRESH_BEFORE:
        return tok
    tok = requests.get(f"{AUTH_URL}/v1/auth/token",
                       auth=(EMAIL, PASSWORD)).json()["token"]
    CACHE["token"] = tok
    return tok

def _seconds_remaining(jwt):
    payload_b64 = jwt.split(".")[1]
    # JWT base64url is unpadded — add padding back before decoding
    payload = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))
    return payload["exp"] - int(time.time())
```

You **don't** need to verify the signature here — you're just reading
your own copy of the claim to decide whether to refresh. The API does
the real verification on every call.

#### Reactive — catch 401, refresh, retry once

Simplest pattern. No JWT parsing required, just one extra round-trip
when expiry actually hits.

```python theme={null}
def call(method, path, **kwargs):
    for attempt in (1, 2):
        r = requests.request(
            method, f"{API_URL}{path}",
            headers={"Authorization": f"Bearer {get_token()}"},
            **kwargs,
        )
        if r.status_code != 401 or attempt == 2:
            return r
        CACHE.pop("token", None)  # invalidate, force refresh
```

Two caveats:

* **Don't retry indefinitely** — retry exactly once. If the second call
  also 401s, the credentials are wrong, not the token.
* A 401 can also mean the token was revoked or your account was
  disabled — same handler still works (you'll get 401 again on retry,
  and surface it).

#### Scheduled rotation

For machine-to-machine integrations where you already have the Basic
credentials stored, the simplest possible setup is a cron that
refreshes weekly or monthly. No expiry math, no retry logic — just a
fresh token always ready.

```bash theme={null}
# /etc/cron.weekly/bsa-token
curl -sf --user "$BSA_EMAIL:$BSA_PASSWORD" \
  https://auth-api.bsa.ai/v1/auth/token \
  | jq -r .token > /var/lib/bsa/token
```

Your app reads `/var/lib/bsa/token` (or whatever store you use) on
every request. The cron ensures it's never within 7 days of expiry.

### Knowing when expiry is coming

If you operate the integration yourself rather than via cron, set a
calendar reminder for \~11 months after each token issuance. The
`iat` claim in the JWT payload gives you the exact issuance timestamp
to anchor on.

### Errors

| HTTP | Cause                                                                                                                                                                                                                              |
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 401  | Missing Basic header, malformed credentials, wrong password, or account disabled. Response body is always `{"code":"unauthenticated","message":"invalid credentials"}` regardless of which check failed (no user-enumeration leak) |
| 405  | Using anything other than `GET`                                                                                                                                                                                                    |

## 2. How authorization works

Every partner-facing endpoint enforces:

1. **Authentication** — a valid, unexpired JWT in the `Authorization`
   header. Failure: `401 unauthenticated`.
2. **Authorization** — the JWT must contain the `ADMIN` role. Failure:
   `401 unauthenticated` (the API does not distinguish "not
   authenticated" from "not authorized" on the wire — both map to 401).

All partner-facing endpoints (`/v1/customers/...`, `/v1/loans/...`,
`/v1/repayments/...`, `/v1/loan-products/...`, `/v1/credit-decisions/...`)
require the `ADMIN` role.

## 3. Use the token

Set the `Authorization` header on every request:

```bash theme={null}
TOKEN=$(curl -sf "https://auth-api.bsa.ai/v1/auth/token" \
  -u "$EMAIL:$PASSWORD" | jq -r .token)

curl -sf "https://api-staging.bsa.ai/v1/customers" \
  -H "Authorization: Bearer $TOKEN"
```

For the per-call mechanics — headers, expiry handling, common 401
causes — see [Authentication](/concepts/authentication).

## Putting it together

<CodeGroup>
  ```bash bash theme={null}
  # 1. Get a token (once per ~year, or whenever the current one expires)
  TOKEN=$(curl -sf "$AUTH_URL/v1/auth/token" \
    -u "$EMAIL:$PASSWORD" | jq -r .token)

  # 2. Use it everywhere
  curl -sf "$API_URL/v1/customers/42" \
    -H "Authorization: Bearer $TOKEN"
  ```

  ```python Python theme={null}
  import requests

  # 1. Get a token
  resp = requests.get(
      f"{AUTH_URL}/v1/auth/token",
      auth=(EMAIL, PASSWORD),
  )
  resp.raise_for_status()
  token = resp.json()["token"]

  # 2. Use it
  session = requests.Session()
  session.headers["Authorization"] = f"Bearer {token}"

  resp = session.get(f"{API_URL}/v1/customers/42")
  ```

  ```javascript JavaScript theme={null}
  // 1. Get a token
  const tokenResp = await fetch(`${AUTH_URL}/v1/auth/token`, {
    headers: {
      "Authorization": "Basic " + btoa(`${EMAIL}:${PASSWORD}`),
    },
  });
  const { token } = await tokenResp.json();

  // 2. Use it
  const resp = await fetch(`${API_URL}/v1/customers/42`, {
    headers: { "Authorization": `Bearer ${token}` },
  });
  ```
</CodeGroup>
