Appearance
Auth & sign-in
Sign in with a password, a passkey or single sign-on; register an account; verify an email address; rotate, refresh and revoke sessions; and recover a password.
Read Authentication first for how sessions, access tokens and the refresh cookie fit together. The endpoints below are the ones the app's Login, Register and Profile → Security screens use.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/token/login/ | Sign in with a password |
| POST | /api/v1/auth/refresh/ | Refresh the access token |
| POST | /api/v1/auth/logout/ | Sign out (this session) |
| POST | /api/v1/auth/logout-all/ | Sign out everywhere |
| POST | /api/v1/users/ | Register an account |
| POST | /users/check/availability/ | Check whether a username or email is free |
| POST | /users/verify/ | Confirm the email one-time code |
| POST | /users/verify/resend/ | Resend the email one-time code |
| GET | /users/verify/number/ | Confirm a mobile verification code |
| POST | /users/verify/number/ | Send a mobile verification code |
| POST | /users/password/change/ | Change password |
| POST | /users/password/set/ | Set a password (SSO accounts) |
| POST | /users/password/forget/ | Reset a forgotten password with the recovery code |
| POST | /users/recovery-code/send-email/ | Email me my recovery code |
| GET | /api/v1/auth/microsoft/authorize/ | Start Microsoft sign-in |
| POST | /api/v1/auth/microsoft/setup-credentials/ | Store an organisation's Microsoft SSO credentials |
| POST | /api/v1/auth/microsoft/callback/ | Complete Microsoft sign-in |
| POST | /api/v1/auth/microsoft/link-exchange/ | Verify a Microsoft account for linking |
| GET | /api/v1/auth/google/authorize/ | Start Google sign-in |
| POST | /api/v1/auth/google/callback/ | Complete Google sign-in |
| POST | /api/v1/auth/google/link-exchange/ | Verify a Google account for linking |
| POST | /users/passkeys/register/begin/ | Start passkey registration |
| POST | /users/passkeys/register/finish/ | Finish passkey registration |
| POST | /users/passkeys/login/discoverable/begin/ | Start passkey sign-in |
| POST | /users/passkeys/login/discoverable/finish/ | Finish passkey sign-in |
| GET | /users/passkeys/ | List my passkeys |
| POST | /users/passkeys/remove/ | Remove a passkey |
POST /api/v1/token/login/
Sign in with a password
Exchange a username (or email address) and password for a session. On success the body carries the access token and the response sets the refresh_token cookie.
When the deployment has EMAIL_VERIFICATION enabled, an account must be both approved (verified) and have a verified email address before it can sign in.
Scripted clients: capture the Set-Cookie header if you intend to refresh the token later, and see Authentication for the full lifecycle.
Auth: None (public) · Rate limit: 10/min per account (username or email) · In the app: Login page
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
username | string | yes | Username or email address (matched exactly). |
password | string | yes |
json
{
"username": "jane@acme.com",
"password": "correct horse battery staple"
}Response 200 — Signed in. Also sets the refresh_token cookie.
| Field | Type | Description |
|---|---|---|
auth_token | string | Access token. Send as Authorization: Token <auth_token>. |
expires_in | integer | Seconds until the access token expires (24 h by default). |
absolute_expires_in | integer | Seconds until the session's hard cap, after which the user must sign in again (90 days by default). Omitted on SSO sign-in responses. |
json
{
"auth_token": "3f9c1b0e4d5a6c7b8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d",
"expires_in": 86400,
"absolute_expires_in": 7776000
}Response 400 — Wrong credentials, disabled or unverified account, or a missing field.
json
{
"non_field_errors": [
"Unable to log in with provided credentials."
]
}Response 429 — Rate limit exceeded. Retry-After tells you how long to wait.
json
{
"detail": "Request was throttled. Expected available in 42 seconds."
}Errors
| Status | Body | When |
|---|---|---|
400 | {"non_field_errors":["Unable to log in with provided credentials."]} | Unknown user or wrong password. |
400 | {"non_field_errors":["User account is disabled."]} | The account is deactivated (e.g. deprovisioned). |
400 | {"non_field_errors":["User account is not verified."]} | Email verification is enforced and an admin has not approved the account yet. |
400 | {"non_field_errors":["Email address is not verified. Please verify your email before logging in."]} | Email verification is enforced and the OTP has not been confirmed. |
400 | {"password":["This field is required."]} | A required field is missing. |
Example
bash
curl -i -X POST "https://api.example.com/api/v1/token/login/" \
-H "Content-Type: application/json" \
-d '{"username": "jane@acme.com", "password": "correct horse battery staple"}'
# HTTP/1.1 200 OK
# Set-Cookie: refresh_token=9d1c...; Path=/api/v1/auth/; HttpOnly; Secure; SameSite=None
# {"auth_token":"3f9c...","expires_in":86400,"absolute_expires_in":7776000}POST /api/v1/auth/refresh/
Refresh the access token
Rotate the session: returns a new access token and sets a new refresh cookie. The old refresh token is invalidated (it remains accepted for a 30-second grace window so concurrent tabs that race each other all receive the same new tokens).
The refresh token is read from the refresh_token cookie; clients that cannot send cookies may put it in the JSON body instead.
A session can be refreshed for at most 90 days after the original sign-in (absolute_expires_in). After that the response is 401 with code: max_session_lifetime_exceeded and the user must sign in again.
Auth: Refresh token (cookie or body) · In the app: Automatic — the app refreshes shortly before the access token expires and on any 401
Request body (application/json, optional)
| Field | Type | Required | Description |
|---|---|---|---|
refresh_token | string | no | Only needed when the cookie is not sent. refresh is accepted as an alias. |
json
{
"refresh_token": "9d1c2b3a4f5e6d7c8b9a0f1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6b7a8f9e0d1c"
}Response 200 — Rotated. Also sets a new refresh_token cookie.
| Field | Type | Description |
|---|---|---|
auth_token | string | Access token. Send as Authorization: Token <auth_token>. |
expires_in | integer | Seconds until the access token expires (24 h by default). |
absolute_expires_in | integer | Seconds until the session's hard cap, after which the user must sign in again (90 days by default). Omitted on SSO sign-in responses. |
json
{
"auth_token": "3f9c1b0e4d5a6c7b8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d",
"expires_in": 86400,
"absolute_expires_in": 7776000
}Response 400 — No refresh token in the cookie or the body.
json
{
"detail": "refresh_token is required (cookie or body).",
"code": "missing_refresh_token"
}Response 401 — Refresh token unknown, expired, revoked, or the session hit its 90-day cap. The cookie is cleared.
json
{
"detail": "Invalid or expired refresh token.",
"code": "invalid_refresh_token"
}Example
bash
# with the cookie jar saved at sign-in
curl -X POST "https://api.example.com/api/v1/auth/refresh/" -b cookies.txt -c cookies.txt
# or, without cookies
curl -X POST "https://api.example.com/api/v1/auth/refresh/" \
-H "Content-Type: application/json" \
-d '{"refresh_token": "9d1c..."}'POST /api/v1/auth/logout/
Sign out (this session)
Revokes the session that owns the presented access token and clears the refresh cookie. Other devices stay signed in.
Auth: Session token · In the app: Profile menu → Log out
Response 204 — Session revoked; refresh_token cookie cleared.
Response 401 — Missing, invalid, expired or revoked access token.
json
{
"detail": "Invalid or expired token."
}Example
bash
curl -X POST "https://api.example.com/api/v1/auth/logout/" \
-H "Authorization: Token $FINBLADE_TOKEN"POST /api/v1/auth/logout-all/
Sign out everywhere
Revokes every active session of the user (all devices and API clients) and clears the refresh cookie.
Auth: Session token · In the app: Profile → Security → Sign out of all devices
Response 204 — All sessions revoked.
Response 401 — Missing, invalid, expired or revoked access token.
json
{
"detail": "Invalid or expired token."
}Example
bash
curl -X POST "https://api.example.com/api/v1/auth/logout-all/" \
-H "Authorization: Token $FINBLADE_TOKEN"POST /api/v1/users/
Register an account
Create a user with a password. The new account:
- receives a one-time code by email (see
POST /users/verify/), - gets the default model and the
Generalrole, - is
verifiedimmediately only when a valid promo code is supplied; otherwise an administrator approves it (the deployment may not enforce this), - is placed in the organisation whose domain matches the email address, or in a personal organisation.
Registration is disabled on deployments that provision users through SSO or SCIM only.
Auth: None (public) · In the app: Register page
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
username | string | yes | |
email | string (email) | yes | |
password | string | yes | Validated with Django's password validators (length, common passwords, similarity to the username). |
first_name | string | no | |
last_name | string | no | |
phone | integer, nullable | no | Mobile number as digits (no +). |
promo_code | string | no | Optional single-use promotional code. |
json
{
"username": "jane",
"email": "jane@acme.com",
"password": "correct horse battery staple",
"first_name": "Jane",
"last_name": "Doe",
"phone": 966500000000
}Response 201 — Account created (password and promo code are not echoed).
| Field | Type | Description |
|---|---|---|
id | integer | |
first_name | string | |
last_name | string | |
username | string | |
email | string | |
phone | integer, nullable |
json
{
"id": 42,
"first_name": "Jane",
"last_name": "Doe",
"username": "jane",
"email": "jane@acme.com",
"phone": 966500000000
}Response 400 — Validation failed.
json
{
"username": [
"A user with that username already exists."
],
"password": [
"This password is too short. It must contain at least 8 characters."
]
}Errors
| Status | Body | When |
|---|---|---|
400 | {"promo_code":["Invalid or already used promo code."]} | The promo code is unknown or has been redeemed. |
Example
bash
curl -X POST "https://api.example.com/api/v1/users/" \
-H "Content-Type: application/json" \
-d '{"username":"jane","email":"jane@acme.com","password":"correct horse battery staple","first_name":"Jane","last_name":"Doe","phone":966500000000}'POST /users/check/availability/
Check whether a username or email is free
Used by the Register page for inline validation. Pass either field or both.
Auth: None (public) · In the app: Register page (as you type)
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
username | string | no | |
email | string | no |
json
{
"username": "jane",
"email": "jane@acme.com"
}Response 200 — One entry per field supplied.
| Field | Type | Description |
|---|---|---|
username | object | |
username.available | boolean | |
username.message | string | |
email | object | |
email.available | boolean | |
email.message | string |
json
{
"username": {
"available": false,
"message": "Username is already taken."
},
"email": {
"available": true,
"message": "Email is available."
}
}Response 400 — Neither field supplied.
json
{
"error": "Please provide a username or email to check."
}Example
bash
curl -X POST "https://api.example.com/users/check/availability/" \
-H "Content-Type: application/json" \
-d '{"username":"jane","email":"jane@acme.com"}'POST /users/verify/
Confirm the email one-time code
Confirms the 6-digit code that registration emailed to the user. Codes expire after 10 minutes. On success email_verified becomes true; if the account still needs administrator approval the message says so and the reviewers are notified.
Auth: None (public) · In the app: Register → email verification step
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
otp | string | yes | The 6-digit code from the email. |
json
{
"otp": "483920"
}Response 200 — Verified.
| Field | Type | Description |
|---|---|---|
message | string |
json
{
"message": "User verified successfully"
}Response 400 — Unknown or expired code.
json
{
"message": "Invalid OTP"
}Example
bash
curl -X POST "https://api.example.com/users/verify/" \
-H "Content-Type: application/json" \
-d '{"otp":"483920"}'POST /users/verify/resend/
Resend the email one-time code
Auth: None (public) · In the app: Register → "Resend code"
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | yes |
json
{
"email": "jane@acme.com"
}Response 200 — Sent, or already verified.
| Field | Type | Description |
|---|---|---|
message | string |
json
{
"message": "OTP sent to your email"
}Response 404 — No account with that email.
json
{
"message": "User not found"
}Example
bash
curl -X POST "https://api.example.com/users/verify/resend/" \
-H "Content-Type: application/json" \
-d '{"email":"jane@acme.com"}'GET /users/verify/number/
Confirm a mobile verification code
Auth: Session token · In the app: Register → mobile verification step
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | The 6-digit code received by SMS. |
Response 200 — Number verified.
| Field | Type | Description |
|---|---|---|
message | string |
json
{
"message": "User number verified"
}Response 400 — token missing.
json
{
"message": "Token is required"
}Response 404 — Code not recognised.
json
{
"message": "User token not found"
}Example
bash
curl -X GET "https://api.example.com/users/verify/number/?token=483920" \
-H "Authorization: Token $FINBLADE_TOKEN"POST /users/verify/number/
Send a mobile verification code
Sends a 6-digit code by SMS/WhatsApp to the phone number on the profile.
Auth: Session token · In the app: Register → mobile verification step
Response 200 — Code sent.
| Field | Type | Description |
|---|---|---|
message | string |
json
{
"message": "Code sent"
}Response 400 — The profile has no phone number.
json
{
"error": "Mobile number is empty"
}Example
bash
curl -X POST "https://api.example.com/users/verify/number/" \
-H "Authorization: Token $FINBLADE_TOKEN"POST /users/password/change/
Change password
For accounts that already have a password. The web app re-wraps the user's encryption keys with the new password itself; this endpoint only updates the password hash.
Auth: Session token · In the app: Profile → Security → Change password
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
old_password | string | yes | |
new_password | string | yes |
json
{
"old_password": "correct horse battery staple",
"new_password": "purple monkey dishwasher 42"
}Response 200 — Updated.
| Field | Type | Description |
|---|---|---|
message | string |
json
{
"message": "Password updated successfully."
}Response 400 — Missing fields, wrong old password, or the new password fails validation (error is then a list of messages).
json
{
"error": "Old password is incorrect."
}Example
bash
curl -X POST "https://api.example.com/users/password/change/" \
-H "Authorization: Token $FINBLADE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"old_password":"correct horse battery staple","new_password":"purple monkey dishwasher 42"}'POST /users/password/set/
Set a password (SSO accounts)
Gives an SSO-only account a local password so it can also sign in with POST /api/v1/token/login/. If the account already has a password, current_password is required. When the account has a passkey, the server also wraps the user's recovery key under the new password so password sign-in can unlock encrypted chats.
Auth: Session token · In the app: Profile → Security → Set password
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
new_password | string | yes | |
current_password | string | no | Required only when a password already exists. |
json
{
"new_password": "purple monkey dishwasher 42"
}Response 200 — Set.
| Field | Type | Description |
|---|---|---|
message | string |
json
{
"message": "Password set successfully."
}Response 400 — Missing/incorrect current password or a weak new password.
json
{
"error": "Current password is required for existing password users."
}Example
bash
curl -X POST "https://api.example.com/users/password/set/" \
-H "Authorization: Token $FINBLADE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"new_password":"purple monkey dishwasher 42"}'POST /users/password/forget/
Reset a forgotten password with the recovery code
Accounts receive a recovery code at sign-up (and can email themselves a fresh copy from Profile). The code is the only thing that can unlock the user's encrypted chat history without the password, so it is verified here by decrypting the stored recovery envelope; a wrong code is reported as invalid without revealing whether recovery is set up.
Auth: None (public) · In the app: Login → "Forgot password"
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | yes | |
recovery_code | string | yes | |
new_password | string | yes |
json
{
"email": "jane@acme.com",
"recovery_code": "K7QF-2M9X-ZP4L-8HTW",
"new_password": "purple monkey dishwasher 42"
}Response 200 — Password reset.
| Field | Type | Description |
|---|---|---|
message | string |
json
{
"message": "Password reset successfully."
}Response 400 — Missing fields, invalid recovery code, or weak password.
json
{
"error": "Invalid recovery code."
}Response 404 — No account with that email.
json
{
"error": "User not found."
}Example
bash
curl -X POST "https://api.example.com/users/password/forget/" \
-H "Content-Type: application/json" \
-d '{"email":"jane@acme.com","recovery_code":"K7QF-2M9X-ZP4L-8HTW","new_password":"purple monkey dishwasher 42"}'POST /users/recovery-code/send-email/
Email me my recovery code
Sends the recovery code to the account's email address. The client supplies the code because the server only ever stores it wrapped and cannot derive it.
Auth: Session token · In the app: Profile → Security → "Email my recovery code"
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
recovery_code | string | yes |
json
{
"recovery_code": "K7QF-2M9X-ZP4L-8HTW"
}Response 200 — Sent.
| Field | Type | Description |
|---|---|---|
message | string |
json
{
"message": "Recovery code sent to your email."
}Response 400 — Missing code or no email on file.
json
{
"error": "Recovery code is required."
}Response 500 — The email provider rejected the message.
json
{
"error": "path is required"
}Example
bash
curl -X POST "https://api.example.com/users/recovery-code/send-email/" \
-H "Authorization: Token $FINBLADE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"recovery_code":"K7QF-2M9X-ZP4L-8HTW"}'GET /api/v1/auth/microsoft/authorize/
Start Microsoft sign-in
Microsoft sign-in is configured per organisation (each organisation registers its own Azure AD application). Pass the user's email domain — or the whole email address — and receive the Microsoft authorisation URL to redirect the browser to. Microsoft redirects back to <FRONTEND_URL>/microsoft-callback with code and state, which the app posts to the callback endpoint.
If the organisation has no credentials yet the response is 404 with need_credentials: true; an administrator can store them with POST /api/v1/auth/microsoft/setup-credentials/.
Auth: None (public) · In the app: Login → "Sign in with Microsoft"
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
domain | string | yes | Email domain (acme.com) or an email address (jane@acme.com). |
Response 200 — Redirect the user to auth_url.
| Field | Type | Description |
|---|---|---|
auth_url | string (uri) |
json
{
"auth_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=…&state=…"
}Response 400 — domain missing.
json
{
"detail": "Authentication credentials were not provided."
}Response 404 — The organisation has no Microsoft credentials.
json
{
"detail": "Microsoft SSO is not configured for this organization.",
"need_credentials": true,
"domain": "acme.com"
}Example
bash
curl -X GET "https://api.example.com/api/v1/auth/microsoft/authorize/?domain=acme.com"POST /api/v1/auth/microsoft/setup-credentials/
Store an organisation's Microsoft SSO credentials
Creates the organisation (if needed) and saves its Azure AD application id and secret so users of that domain can sign in with Microsoft. The Azure app must have <FRONTEND_URL>/microsoft-callback registered as a redirect URI.
Auth: None (public) — intended for first-time organisation setup · In the app: Login → "Sign in with Microsoft" → credentials form (shown when need_credentials is true)
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
domain | string | yes | |
client_id | string | yes | Azure AD application (client) id. |
client_secret | string | yes | |
organization_name | string | no | Defaults to a name derived from the domain. |
json
{
"domain": "acme.com",
"organization_name": "Acme Corp",
"client_id": "6c3b0a1e-…",
"client_secret": "…"
}Response 200 — Saved.
| Field | Type | Description |
|---|---|---|
message | string | |
domain | string | |
organization_name | string |
json
{
"message": "Microsoft SSO credentials saved for this organization.",
"domain": "acme.com",
"organization_name": "Acme Corp"
}Response 400 — Missing fields.
json
{
"detail": "Authentication credentials were not provided."
}Example
bash
curl -X POST "https://api.example.com/api/v1/auth/microsoft/setup-credentials/" \
-H "Content-Type: application/json" \
-d '{"domain":"acme.com","organization_name":"Acme Corp","client_id":"6c3b0a1e-…","client_secret":"…"}'POST /api/v1/auth/microsoft/callback/
Complete Microsoft sign-in
Exchanges the authorisation code for the user's identity, finds or creates the Finblade account (matched on the stable Microsoft sub, then by email), and issues a session. New users are provisioned just-in-time into the organisation for the domain, subject to the organisation's seat limit.
GET with the same parameters as a query string is also accepted, for deployments whose redirect URI points at the API.
Auth: None (public) · In the app: /microsoft-callback page
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | Authorisation code from Microsoft. |
state | string | yes | The state Microsoft returned (carries the organisation domain). |
json
{
"code": "0.AXcA…",
"state": "eyJkb21haW4iOiAiYWNtZS5jb20ifQ"
}Response 200 — Signed in. Also sets the refresh_token cookie.
| Field | Type | Description |
|---|---|---|
auth_token | string | |
expires_in | integer | |
user | object | The signed-in user, as returned by GET /users/me/ (info) and SSO callbacks (user). |
user.id | integer | |
user.first_name | string | |
user.last_name | string | |
user.username | string | |
user.email | string (email) | |
user.phone | integer, nullable | Mobile number as digits, or null. |
user.llm_model | string | Display name of the user's default model ("" when unset). |
user.language | string ("en", "ar") | UI language. |
user.organization | string | Organisation name ("" when the user has none). |
user.last_login | string (date-time), nullable | |
user.verified | boolean | Account approved (true for promo-code sign-ups, otherwise set by an admin). |
user.email_verified | boolean | |
user.use_cloud | boolean | |
user.number_verified | boolean | |
user.is_first_time | boolean | True until the app's first-run flow has been acknowledged (POST /users/me/). |
user.roles | string[] | Role names, e.g. ["General"]. |
user.is_staff | boolean | Django staff flag — what admin-only endpoints check. |
user.dedicated_agent | boolean | Whether the Dedicated Agent module is enabled for this user. |
user.date_joined | string (date-time) | |
user.is_sso | boolean | True when the account has no usable password (SSO-only). |
user.auth_methods | object | Which sign-in methods the account currently has. |
user.auth_methods.password | boolean | |
user.auth_methods.passkey | boolean | |
user.auth_methods.microsoft | boolean | |
user.plan | object, nullable | Active subscription plan (hosted deployments only; null when there is none). |
user.plan.name | string | |
user.plan.code | string | Stable plan key, e.g. pro. |
user.plan.monthly_price | string | |
user.plan.yearly_price | string | |
user.plan.active_until | string (date-time), nullable | |
user.plan.owner | boolean | Present on organisation plans — true if this user owns the subscription. |
user.entitlements | object | Limits resolved from the plan, plus usage counters. null in any quota means unlimited (and is different from 0, a real limit of none). Accounts with no subscription resolve to the built-in free tier with owner_type: none. |
user.entitlements.plan_code | string | Resolved plan key ("" on the free tier). |
user.entitlements.plan_name | string | |
user.entitlements.owner_type | string ("organization", "user", "none") | Who the quota pool belongs to. |
user.entitlements.is_active | boolean | |
user.entitlements.seats | integer | |
user.entitlements.support_tier | string | |
user.entitlements.features | object | Capability flags, e.g. flowapps, dashboards, chat_ai, aes256, byo_llm, rbac, global_secrets, sso_scim, audit_logs, on_prem, data_integration. |
user.entitlements.monthly_workflow_runs | integer, nullable | |
user.entitlements.included_tokens | integer, nullable | |
user.entitlements.storage_bytes | integer, nullable | |
user.entitlements.run_history_days | integer, nullable | |
user.entitlements.live_build_sessions | integer, nullable | |
user.entitlements.workflow_runs_used | integer | Runs this calendar month. |
user.entitlements.storage_used_bytes | integer | Last crawl plus bytes reserved since. |
user.entitlements.storage_breakdown | object | Measured footprint from the last storage crawl. |
user.entitlements.storage_breakdown.media | integer | |
user.entitlements.storage_breakdown.vector | integer | |
user.entitlements.storage_breakdown.workflow | integer | |
user.entitlements.storage_breakdown.total | integer | |
user.entitlements.storage_breakdown.limit | integer, nullable | |
user.entitlements.storage_breakdown.measured_at | string (date-time), nullable | |
user.entitlements.enforced | boolean | Whether the feature gate is switched on for this deployment. |
user.entitlements.storage_enforced | boolean | Whether exceeding storage_bytes actually blocks uploads. |
user.plan_required | boolean | True when the deployment requires an active plan and the app should send plan-less users to the plans page. |
encryption_key | string | The user's data encryption key, returned only when the account has a passkey-wrapped key so the app can unlock chats without a migration prompt. |
id_token | string | Microsoft id token, returned once (not stored). |
json
{
"auth_token": "3f9c…",
"expires_in": 86400,
"user": {
"id": 42,
"username": "jane@acme.com",
"email": "jane@acme.com",
"organization": "Acme Corp",
"is_sso": true
},
"id_token": "eyJ0eXAiOiJKV1Qi…"
}Response 400 — Missing code/state, or Microsoft rejected the exchange.
json
{
"detail": "Authentication credentials were not provided."
}Response 403 — The organisation has no free seats for a new user.
json
{
"detail": "Authentication credentials were not provided."
}Response 404 — The organisation in state has no Microsoft credentials.
json
{
"detail": "Authentication credentials were not provided."
}Example
bash
curl -X POST "https://api.example.com/api/v1/auth/microsoft/callback/" \
-H "Content-Type: application/json" \
-d '{"code":"0.AXcA…","state":"eyJkb21haW4iOiAiYWNtZS5jb20ifQ"}'POST /api/v1/auth/microsoft/link-exchange/
Verify a Microsoft account for linking
For a signed-in user who wants to attach a Microsoft account to their existing profile. Exchanges the code and returns the verified identity claims without creating a session (so the browser cannot be handed another user's session). The client then calls POST /users/sso/link/.
If the verified email belongs to the organisation's domain and the user is still in a personal organisation, they are moved into the organisation.
Auth: Session token · In the app: Profile → Connected accounts → Link Microsoft
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | |
state | string | yes |
Response 200 — Verified claims.
| Field | Type | Description |
|---|---|---|
provider | string ("microsoft") | |
sub | string | Stable Microsoft account id. |
email | string | |
id_token | string | |
organization | string | The user's (possibly updated) organisation name. |
organization_changed | boolean |
json
{
"provider": "microsoft",
"sub": "00000000-0000-0000-a1b2-c3d4e5f60718",
"email": "jane@acme.com",
"id_token": "eyJ0eXAiOiJKV1Qi…",
"organization": "Acme Corp",
"organization_changed": false
}Response 400 — Missing code, bad state, or no stable id returned.
json
{
"detail": "Authentication credentials were not provided."
}Response 502 — Microsoft could not be reached.
json
{
"detail": "Authentication credentials were not provided."
}Response 503 — Microsoft sign-in is not configured for the organisation.
json
{
"detail": "Authentication credentials were not provided."
}Example
bash
curl -X POST "https://api.example.com/api/v1/auth/microsoft/link-exchange/" \
-H "Authorization: Token $FINBLADE_TOKEN"GET /api/v1/auth/google/authorize/
Start Google sign-in
Google sign-in uses one platform-wide OAuth client, so no organisation setup is needed. Returns the consent URL (with prompt=select_account) and a signed state. Google redirects back to <FRONTEND_URL>/google-callback.
Auth: None (public) · In the app: Login → "Sign in with Google"
Response 200 — Redirect the user to auth_url.
| Field | Type | Description |
|---|---|---|
auth_url | string (uri) | |
state | string | Signed, single-use state (valid for a few minutes). |
json
{
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=…&state=…",
"state": "eyJuIjoi…"
}Response 503 — Google sign-in is not configured on this server.
json
{
"detail": "Authentication credentials were not provided."
}Example
bash
curl -X GET "https://api.example.com/api/v1/auth/google/authorize/"POST /api/v1/auth/google/callback/
Complete Google sign-in
Exchanges the code, verifies the Google id token against Google's published keys, matches the account on the stable Google sub (never on email alone), and issues a session. Unverified Google email addresses are refused; new users from a Google Workspace domain that matches an organisation are placed in it, subject to the seat limit.
Auth: None (public) · In the app: /google-callback page
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | |
state | string | yes | The state from the authorize step. |
Response 200 — Signed in. Also sets the refresh_token cookie.
| Field | Type | Description |
|---|---|---|
auth_token | string | |
expires_in | integer | |
user | object | The signed-in user, as returned by GET /users/me/ (info) and SSO callbacks (user). |
user.id | integer | |
user.first_name | string | |
user.last_name | string | |
user.username | string | |
user.email | string (email) | |
user.phone | integer, nullable | Mobile number as digits, or null. |
user.llm_model | string | Display name of the user's default model ("" when unset). |
user.language | string ("en", "ar") | UI language. |
user.organization | string | Organisation name ("" when the user has none). |
user.last_login | string (date-time), nullable | |
user.verified | boolean | Account approved (true for promo-code sign-ups, otherwise set by an admin). |
user.email_verified | boolean | |
user.use_cloud | boolean | |
user.number_verified | boolean | |
user.is_first_time | boolean | True until the app's first-run flow has been acknowledged (POST /users/me/). |
user.roles | string[] | Role names, e.g. ["General"]. |
user.is_staff | boolean | Django staff flag — what admin-only endpoints check. |
user.dedicated_agent | boolean | Whether the Dedicated Agent module is enabled for this user. |
user.date_joined | string (date-time) | |
user.is_sso | boolean | True when the account has no usable password (SSO-only). |
user.auth_methods | object | Which sign-in methods the account currently has. |
user.auth_methods.password | boolean | |
user.auth_methods.passkey | boolean | |
user.auth_methods.microsoft | boolean | |
user.plan | object, nullable | Active subscription plan (hosted deployments only; null when there is none). |
user.plan.name | string | |
user.plan.code | string | Stable plan key, e.g. pro. |
user.plan.monthly_price | string | |
user.plan.yearly_price | string | |
user.plan.active_until | string (date-time), nullable | |
user.plan.owner | boolean | Present on organisation plans — true if this user owns the subscription. |
user.entitlements | object | Limits resolved from the plan, plus usage counters. null in any quota means unlimited (and is different from 0, a real limit of none). Accounts with no subscription resolve to the built-in free tier with owner_type: none. |
user.entitlements.plan_code | string | Resolved plan key ("" on the free tier). |
user.entitlements.plan_name | string | |
user.entitlements.owner_type | string ("organization", "user", "none") | Who the quota pool belongs to. |
user.entitlements.is_active | boolean | |
user.entitlements.seats | integer | |
user.entitlements.support_tier | string | |
user.entitlements.features | object | Capability flags, e.g. flowapps, dashboards, chat_ai, aes256, byo_llm, rbac, global_secrets, sso_scim, audit_logs, on_prem, data_integration. |
user.entitlements.monthly_workflow_runs | integer, nullable | |
user.entitlements.included_tokens | integer, nullable | |
user.entitlements.storage_bytes | integer, nullable | |
user.entitlements.run_history_days | integer, nullable | |
user.entitlements.live_build_sessions | integer, nullable | |
user.entitlements.workflow_runs_used | integer | Runs this calendar month. |
user.entitlements.storage_used_bytes | integer | Last crawl plus bytes reserved since. |
user.entitlements.storage_breakdown | object | Measured footprint from the last storage crawl. |
user.entitlements.storage_breakdown.media | integer | |
user.entitlements.storage_breakdown.vector | integer | |
user.entitlements.storage_breakdown.workflow | integer | |
user.entitlements.storage_breakdown.total | integer | |
user.entitlements.storage_breakdown.limit | integer, nullable | |
user.entitlements.storage_breakdown.measured_at | string (date-time), nullable | |
user.entitlements.enforced | boolean | Whether the feature gate is switched on for this deployment. |
user.entitlements.storage_enforced | boolean | Whether exceeding storage_bytes actually blocks uploads. |
user.plan_required | boolean | True when the deployment requires an active plan and the app should send plan-less users to the plans page. |
id_token | string | Google id token, returned once. |
sso_rek_wrapped | string, nullable | The user's recovery key wrapped for this Google identity — null until Google was linked from Profile. |
Response 400 — Missing code, invalid/expired state, or Google rejected the sign-in.
json
{
"detail": "Authentication credentials were not provided."
}Response 403 — Unverified Google email, or the organisation has no free seats.
json
{
"detail": "Authentication credentials were not provided."
}Response 502 — Google could not be reached.
json
{
"detail": "Authentication credentials were not provided."
}Response 503 — Google sign-in is not configured.
json
{
"detail": "Authentication credentials were not provided."
}Example
bash
curl -X POST "https://api.example.com/api/v1/auth/google/callback/"POST /api/v1/auth/google/link-exchange/
Verify a Google account for linking
Signed-in counterpart of the callback: returns the verified Google claims without creating a session, so the profile can link the account with POST /users/sso/link/. A verified Workspace address that matches an organisation's domain moves a personal-organisation user into it.
Auth: Session token · In the app: Profile → Connected accounts → Link Google
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | |
state | string | yes |
Response 200 — Verified claims.
| Field | Type | Description |
|---|---|---|
provider | string ("google") | |
sub | string | |
email | string | |
email_verified | boolean | |
hd | string | Google Workspace hosted domain ("" for consumer accounts). |
id_token | string | |
organization | string | |
organization_changed | boolean |
json
{
"provider": "google",
"sub": "112233445566778899000",
"email": "jane@acme.com",
"email_verified": true,
"hd": "acme.com",
"id_token": "eyJhbGciOiJSUzI1NiIs…",
"organization": "Acme Corp",
"organization_changed": false
}Response 400 — Missing code or invalid state.
json
{
"detail": "Authentication credentials were not provided."
}Example
bash
curl -X POST "https://api.example.com/api/v1/auth/google/link-exchange/" \
-H "Authorization: Token $FINBLADE_TOKEN"POST /users/passkeys/register/begin/
Start passkey registration
Returns WebAuthn creation options for navigator.credentials.create(). The challenge is valid for 5 minutes. A user can hold at most 5 active passkeys.
Auth: Session token · In the app: Profile → Security → Add passkey
Response 200 — Options to pass to the browser.
| Field | Type | Description |
|---|---|---|
options | object | PublicKeyCredentialCreationOptions (JSON, base64url binary fields): rp, user, challenge, pubKeyCredParams, excludeCredentials, authenticatorSelection, timeout. |
Response 409 — Passkey limit reached.
json
{
"detail": "You can have at most 5 passkeys. Remove one before adding another.",
"code": "PASSKEY_LIMIT_REACHED",
"limit": 5
}Example
bash
curl -X POST "https://api.example.com/users/passkeys/register/begin/" \
-H "Authorization: Token $FINBLADE_TOKEN"POST /users/passkeys/register/finish/
Finish passkey registration
Verifies the attestation returned by the browser and stores the credential. The web app also sends a key envelope — the user's encryption keys wrapped so that a later passkey sign-in can unlock encrypted chats without a password.
Auth: Session token · In the app: Profile → Security → Add passkey
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
credential | object | yes | A WebAuthn PublicKeyCredential as produced by the browser, JSON-serialised (navigator.credentials.get() / .create() output with base64url-encoded binary fields). |
credential.id | string | yes | Credential id (base64url). |
credential.rawId | string | no | Same as id. |
credential.type | string ("public-key") | yes | |
credential.response | object | yes | clientDataJSON plus, for registration, attestationObject and transports; for sign-in, authenticatorData, signature and userHandle. |
name | string | no | Label shown in the passkey list. Default Passkey. |
key_envelope | object | no | Client-wrapped key material (opaque to the API). |
rek_plaintext | string | no | Recovery key, wrapped by the server at rest (web app only). |
dek_plaintext | string | no | Data encryption key, wrapped by the server at rest (web app only). |
Response 200 — Registered.
| Field | Type | Description |
|---|---|---|
ok | boolean | |
passkey_debug | object | Challenge diagnostics (safe to ignore). |
json
{
"ok": true
}Response 400 — Challenge expired or attestation invalid.
json
{
"detail": "Authentication credentials were not provided."
}Response 409 — Passkey limit reached.
json
{
"detail": "Authentication credentials were not provided."
}Example
bash
curl -X POST "https://api.example.com/users/passkeys/register/finish/" \
-H "Authorization: Token $FINBLADE_TOKEN"POST /users/passkeys/login/discoverable/begin/
Start passkey sign-in
"Discoverable" (usernameless) sign-in: returns assertion options for navigator.credentials.get() with user verification required. The browser picks the passkey, so no username is needed.
Auth: None (public) · In the app: Login → "Sign in with a passkey"
Response 200 — Options to pass to the browser.
| Field | Type | Description |
|---|---|---|
options | object | PublicKeyCredentialRequestOptions (JSON): challenge, rpId, timeout, userVerification. |
Example
bash
curl -X POST "https://api.example.com/users/passkeys/login/discoverable/begin/"POST /users/passkeys/login/discoverable/finish/
Finish passkey sign-in
Verifies the assertion, issues a session (body + refresh cookie), and — when the passkey holds a key envelope — returns the user's data encryption key so the app can decrypt chats immediately.
Auth: None (public) · In the app: Login → "Sign in with a passkey"
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
credential | object | yes | A WebAuthn PublicKeyCredential as produced by the browser, JSON-serialised (navigator.credentials.get() / .create() output with base64url-encoded binary fields). |
credential.id | string | yes | Credential id (base64url). |
credential.rawId | string | no | Same as id. |
credential.type | string ("public-key") | yes | |
credential.response | object | yes | clientDataJSON plus, for registration, attestationObject and transports; for sign-in, authenticatorData, signature and userHandle. |
Response 200 — Signed in. Also sets the refresh_token cookie.
| Field | Type | Description |
|---|---|---|
auth_token | string | |
expires_in | integer | |
absolute_expires_in | integer | |
username | string | The account's email (or username) so the app knows who signed in. |
encryption_key | string, nullable | Data encryption key (hex), or null when the passkey has no envelope. |
encryption_key_debug | object | Where the key was found (diagnostics). |
json
{
"auth_token": "3f9c…",
"expires_in": 86400,
"absolute_expires_in": 7776000,
"username": "jane@acme.com",
"encryption_key": "9a3f…"
}Response 400 — Challenge expired, unknown credential, or the assertion failed verification.
json
{
"detail": "Passkey login challenge expired. Start again."
}Example
bash
curl -X POST "https://api.example.com/users/passkeys/login/discoverable/finish/"GET /users/passkeys/
List my passkeys
Auth: Session token · In the app: Profile → Security
Response 200 — Active passkeys.
| Field | Type | Description |
|---|---|---|
passkeys | object[] | |
passkeys[].credential_id | string | |
passkeys[].name | string | |
passkeys[].created_at | string (date-time) | |
passkeys[].last_used_at | string (date-time), nullable | |
limit | integer | |
can_add | boolean | |
auth_method_count | integer | Password + passkeys + linked providers. When 1, the last method cannot be removed. |
json
{
"passkeys": [
{
"credential_id": "pQECAyYgASFYIH…",
"name": "MacBook Touch ID",
"created_at": "2026-03-02T09:12:44Z",
"last_used_at": "2026-09-20T07:01:03Z"
}
],
"limit": 5,
"can_add": true,
"auth_method_count": 2
}Example
bash
curl -X GET "https://api.example.com/users/passkeys/" \
-H "Authorization: Token $FINBLADE_TOKEN"POST /users/passkeys/remove/
Remove a passkey
Removes one passkey (credential_id) or, when no id is given, all of them. Refused if that would leave the account with no way to sign in.
Auth: Session token · In the app: Profile → Security → passkey "Remove"
Request body (application/json, optional)
| Field | Type | Required | Description |
|---|---|---|---|
credential_id | string | no | Omit to remove every passkey. |
json
{
"credential_id": "pQECAyYgASFYIH…"
}Response 200 — Removed.
| Field | Type | Description |
|---|---|---|
ok | boolean | |
removed | integer |
json
{
"ok": true,
"removed": 1
}Response 404 — No matching active passkey.
json
{
"detail": "Authentication credentials were not provided."
}Response 409 — It is the account's only sign-in method.
json
{
"detail": "This is your only way to sign in. Add a password or another sign-in method before removing it.",
"code": "PASSKEY_LAST_AUTH_METHOD"
}Example
bash
curl -X POST "https://api.example.com/users/passkeys/remove/" \
-H "Authorization: Token $FINBLADE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"credential_id":"pQECAyYgASFYIH…"}'