Authentication and sessions
Every endpoint except registration, login, and the password and
verification flows requires a bearer credential in the Authorization
header. There are two kinds.
JWT sessions — for an app with a signed-in user
This section is the whole contract: a client written from this page alone is correct.
Signing in
POST /api/v1/auth/login with the user’s credentials and, optionally,
whether to keep them signed in:
It returns a token pair:
Send the access token on every request:
When login is refused
A refused login answers 401 with a detail to show the user and a
code to branch on:
Only a correct password gets email_not_verified or
account_deactivated, so none of these codes tells a caller without the
password whether an email is registered. Branch on code, not on the
wording in detail.
How long a session lasts
A session is everything from one sign-in to the next. It ends at whichever of two limits comes first:
- Idle timeout. Every refresh pushes the session’s expiry to one idle period from now, so a session in use stays signed in and one left alone ends.
- Hard cap. However active it is, a session ends this long after sign-in. Refreshing never moves it: every rotated refresh token carries the original sign-in time.
remember_me at sign-in picks the pair, and it holds for the life of the
session:
These are the current defaults.
- An access token lasts about an hour (current default), and never outlives its session: near the end of a session, the access token expires when the session does.
- Don’t hardcode any of these numbers. They are server configuration.
If you need to show when a session will end if left alone, read the
expclaim of the latest refresh token;session_startis when the session signed in. A client that never reads a claim at all and simply follows the rules below is also correct.
Carrying the tokens
Login and refresh return both tokens in the JSON body. Refresh and
logout take the refresh token in the body, and every request sends
Authorization: Bearer <access>. No cookies or CSRF handling are
involved.
Refreshing
When the API answers 401, exchange the refresh token at
POST /api/v1/auth/refresh with {"refresh": "<refresh_token>"}. The
response is a new pair, {"access": "...", "refresh": "..."}.
Every refresh rotates the refresh token: the one you sent is spent,
and the response carries its successor. A spent token is not dead
straight away. For a short grace window (currently 30 seconds)
after it was spent, presenting it again returns the same successor
the first refresh issued, not a new one. After the window it is refused
with 401.
That window is what keeps a client simple. Concurrent refreshes converge, and a lost response can be retried.
The rules a client implements
- On a
401from any endpoint other than refresh, refresh once, then retry the original request once with the new access token. If the retried request is also401, or the refresh itself is401, the session is over: discard both tokens and sign the user out. - Store the refresh token from every refresh response, replacing the one you sent. Concurrent refreshes return the same refresh token, so it does not matter which response you store last.
- Concurrent requests need no coordination. Several requests that
hit
401together may each refresh with the same token; within the grace window they all receive the same successor and all succeed. Sharing one in-flight refresh (“single-flight”) is a sensible optimisation, but correctness does not depend on it. - A refresh that fails without a response may be retried with the
same token, promptly. If the server did rotate, the retry inside the
grace window returns the same successor. Retry for a few seconds,
not minutes. Once the window has passed, the answer is
401. 429from refresh is not a sign-out. Refresh is rate limited per user, not per IP, so colleagues behind one office connection do not share a budget. Back off and try again. Only401ends a session.
Signing out
POST /api/v1/auth/logout with the access token as the bearer and
{"refresh": "<refresh_token>"} in the body ends the session
immediately. There is no grace window on logout. If the refresh token
you hold was spent moments ago by a concurrent refresh, logout still
ends the session: it revokes the successor too.
The access token stays valid until it expires, which is inherent to
stateless JWT, so discard your own copy of both tokens whatever the
response. A 400 means the refresh token was already unusable. The
session is over either way.
Where to keep the tokens
- Native apps: in the platform’s secure store (the iOS Keychain, or Android Keystore-backed encrypted storage). Never plain preferences or a file.
- Browser apps: keep the access token in memory rather than
localStorage, where an injected script could copy it.
API tokens — for scripting without a browser
A long-lived finput_sk_... token, minted at POST /api/v1/api-tokens,
goes in the same header. The secret is shown exactly once, when the
token is created.
A token’s scope is about effect, not HTTP method: read covers every
GET plus the five computation endpoints, and read_write adds the
endpoints that mutate. A token is always bounded by the intersection
with its owner’s own permissions, and privilege-sensitive endpoints —
passwords, user management, billing changes, token management,
connection management — refuse token auth outright whatever the scope.
See the API Tokens endpoints for the full scope rules.