Skip to main content

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.

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.

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. 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:
PurposeHost
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

Authorization: Basic <base64(email:password)>
Most HTTP clients do this for you — curl -u email:password or requests.get(..., auth=(email, password)).

Example

curl -sf "https://auth-api.bsa.ai/v1/auth/token" \
  -u "ada@example.com:correct-horse-battery-staple"

Response

{
  "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:
{
  "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).
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.
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.
# /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

HTTPCause
401Missing 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)
405Using 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:
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.

Putting it together

# 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"