Authentication Overview
SEND authenticates with bearer tokens, not with cookies. Signing in returns a JWT that you attach to every subsequent request as an Authorization: Bearer header. No cookie is ever set and nothing on the API depends on one, so a client that only manages cookies will be treated as signed out on every call.
Two kinds of credential travel in that same header, and the server tells them apart by prefix. Anything beginning with ck_ is validated as an API key; anything else is validated as a JWT access token.
Logging In
Post your credentials to the login route. On success the response body carries the tokens; storing them and sending them back is entirely the client's job.
- POST /api/v1/auth/login with { "username": "...", "password": "..." }
- On success: 200 with { user, accessToken, refreshToken, tokenType, expiresIn }. expiresIn is 1800 -- the access token's lifetime in seconds
- Send accessToken on every following request as Authorization: Bearer <accessToken>
- On failure: 401. Every credential failure reads "invalid credentials", whether or not the username exists
- With two-factor enabled the response carries requires2FA and a challengeToken instead of tokens -- finish the sign-in by presenting the code with that challenge
Token Lifetimes
Two tokens, two fixed lifetimes, both set at the moment of issue. Expiry is absolute: nothing extends a token because you are still making requests, and nothing ends it early because you paused.
- Access token: 30 minutes, reported as expiresIn: 1800 on the sign-in response
- Refresh token: 30 days
- Both lifetimes are fixed in the build -- there is no per-account, per-device or per-key setting
- A valid signature is not sufficient on its own: every authenticated request reloads the account, so a deactivated account stops working on its very next request rather than when its token runs out
Refreshing an Access Token
When the 30 minutes are up, exchange the refresh token for a new pair rather than signing in again.
- POST /api/v1/auth/refresh with { "refreshToken": "..." }. The response carries a fresh accessToken and refreshToken
- Every refresh issues a new refresh token, so a client in continuous use rolls forward indefinitely and a client left idle stops working 30 days after its most recent one
- A failed refresh is terminal -- discard both tokens and sign in again
- The SEND web client does this silently: a 401 triggers one refresh and then replays the original request, and several requests failing at once queue behind that single refresh instead of each firing their own
Error Handling
Authentication errors return standard HTTP status codes. Handle these in your integration to provide a good user experience.
- 401 Unauthorized -- the access token is missing, expired, or no longer matches the account. Refresh once; if the refresh fails too, sign in again
- 403 Forbidden -- authenticated, but not permitted to touch that resource
- 429 Too Many Requests -- rate limit exceeded. Reads are 120 per minute, writes 60, heavy writes 5. Back off using the Retry-After header