Skip to content

Authentication

Finblade authenticates API calls with session access tokens. You obtain one by signing in, send it on every request, refresh it before it expires and revoke it when you are done. This is exactly what the web app does; there is no separate API-key mechanism today.

No personal API keys (yet)

Every API client — a script, a BI tool, an integration — signs in as a user and acts as that user, with that user's data and permissions. For unattended integrations create a dedicated service user in your organisation and sign in with its credentials. Webhook triggers are the exception: they use their own credential and need no session (see Webhooks).

The session model

Signing in creates a session with three clocks:

TokenLifetimeWhere it livesWhat it is for
Access token24 hoursResponse body (auth_token)Sent on every request as Authorization: Token <token>
Refresh token7 days, sliding — renewed on every refreshrefresh_token cookie (httpOnly), also accepted in a request bodyExchanged for a new access + refresh pair
Absolute lifetime90 days from sign-in, never extendedabsolute_expires_in in the sign-in responseAfter this the session cannot be refreshed; sign in again

Lifetimes are deployment settings (SESSION_ACCESS_TOKEN_EXPIRY_SECONDS, SESSION_REFRESH_TOKEN_EXPIRY_SECONDS, SESSION_ABSOLUTE_LIFETIME_SECONDS); the values above are the defaults. Multiple sessions per user are allowed by default — each device, browser or script gets its own — unless the deployment enables single-session mode, in which case a new sign-in revokes the others.

Signing in

POST /api/v1/token/login/ with username (a username or an email address) and password:

bash
curl -i -X POST "https://api.example.com/api/v1/token/login/" \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{"username": "jane@acme.com", "password": "•••••••••"}'
http
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: refresh_token=9d1c2b3a…; Path=/api/v1/auth/; Max-Age=604800; HttpOnly; Secure; SameSite=None

{"auth_token": "3f9c1b0e4d5a…", "expires_in": 86400, "absolute_expires_in": 7776000}

The refresh token is only in the cookie. Non-browser clients must capture the Set-Cookie header (curl's -c cookies.txt, requests.Session in Python, a cookie jar in most HTTP libraries) if they intend to refresh. If you do not, simply sign in again when the access token expires — that is fine for scripts that run for less than a day.

Sign-in is rate-limited to 10 attempts per minute per account. Wrong credentials return 400 with non_field_errors; see the endpoint reference for every error case.

Other ways to obtain a session — passkeys, Microsoft and Google single sign-on — produce the same response shape and cookie. They require a browser for the provider's consent page, so they are not suitable for scripts.

Sending the token

Put the access token in the Authorization header with the Token scheme (case-insensitive; a bare token without the scheme is also accepted):

http
Authorization: Token 3f9c1b0e4d5a…

A request without a valid token gets 401 with {"detail": "Authentication credentials were not provided."} or {"detail": "Invalid or expired token."}. A valid token without permission for the resource gets 403 or 404 (many endpoints answer 404 for objects you cannot see, so their existence is not revealed).

Refreshing

Before the access token expires — the web app does it a few minutes early and on any 401 — call POST /api/v1/auth/refresh/:

bash
# with the cookie jar saved at sign-in
curl -s -X POST "https://api.example.com/api/v1/auth/refresh/" -b cookies.txt -c cookies.txt

# without cookies: send the refresh token in the body
curl -s -X POST "https://api.example.com/api/v1/auth/refresh/" \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "9d1c2b3a…"}'

Every refresh rotates both tokens: the response carries a new access token and sets a new refresh cookie; the old refresh token stops working (after a 30-second grace window that lets concurrent clients settle on the same new pair). The old access token stops working immediately.

Failure modes:

ResponseMeaningWhat to do
401 code: invalid_refresh_tokenUnknown, expired or already-rotated refresh tokenSign in again
401 code: max_session_lifetime_exceeded90 days since sign-inSign in again
400 code: missing_refresh_tokenNeither cookie nor body carried a tokenFix the client

Signing out

  • POST /api/v1/auth/logout/ revokes the calling session only.
  • POST /api/v1/auth/logout-all/ revokes every session of the user — useful after a credential leak, and what "Sign out of all devices" does.

Deactivating a user (for example through SCIM) revokes their sessions too.

Sign-in flow for the web app (for comparison)

The app stores the access token in memory/local storage, relies on the httpOnly refresh cookie for rotation, refreshes proactively, retries a request once after a 401, and treats 403 as "not allowed" rather than "signed out". Follow the same pattern in long-running clients.

Two-factor and verification

Where the deployment enforces email verification, a newly registered account must confirm the one-time code from its welcome email (POST /users/verify/) — and, on some deployments, be approved by an administrator — before token/login/ succeeds. The API's password sign-in has no second factor of its own: the web app can add an authenticator-app (TOTP) step in front of it, and passkeys and SSO provide phishing-resistant alternatives, but a script that holds the password gets a token directly.

Security notes

  • Always use HTTPS. Tokens are bearer credentials: anyone holding one is you.
  • Never put the access token in a URL. To let a browser or another system fetch a file, use a signed media URL or a permanent link.
  • Store refresh tokens like passwords. Prefer re-authenticating over persisting a refresh token in a shared location.
  • If you suspect a token has leaked, call logout-all/ (or use "Sign out of all devices" in the app) and sign in again.

Finblade documentation