Skip to content

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

MethodPathPurpose
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)

FieldTypeRequiredDescription
usernamestringyesUsername or email address (matched exactly).
passwordstringyes
json
{
  "username": "jane@acme.com",
  "password": "correct horse battery staple"
}

Response 200 — Signed in. Also sets the refresh_token cookie.

FieldTypeDescription
auth_tokenstringAccess token. Send as Authorization: Token <auth_token>.
expires_inintegerSeconds until the access token expires (24 h by default).
absolute_expires_inintegerSeconds 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

StatusBodyWhen
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)

FieldTypeRequiredDescription
refresh_tokenstringnoOnly 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.

FieldTypeDescription
auth_tokenstringAccess token. Send as Authorization: Token <auth_token>.
expires_inintegerSeconds until the access token expires (24 h by default).
absolute_expires_inintegerSeconds 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 General role,
  • is verified immediately 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)

FieldTypeRequiredDescription
usernamestringyes
emailstring (email)yes
passwordstringyesValidated with Django's password validators (length, common passwords, similarity to the username).
first_namestringno
last_namestringno
phoneinteger, nullablenoMobile number as digits (no +).
promo_codestringnoOptional 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).

FieldTypeDescription
idinteger
first_namestring
last_namestring
usernamestring
emailstring
phoneinteger, 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

StatusBodyWhen
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)

FieldTypeRequiredDescription
usernamestringno
emailstringno
json
{
  "username": "jane",
  "email": "jane@acme.com"
}

Response 200 — One entry per field supplied.

FieldTypeDescription
usernameobject
username.availableboolean
username.messagestring
emailobject
email.availableboolean
email.messagestring
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)

FieldTypeRequiredDescription
otpstringyesThe 6-digit code from the email.
json
{
  "otp": "483920"
}

Response 200 — Verified.

FieldTypeDescription
messagestring
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)

FieldTypeRequiredDescription
emailstring (email)yes
json
{
  "email": "jane@acme.com"
}

Response 200 — Sent, or already verified.

FieldTypeDescription
messagestring
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

FieldTypeRequiredDescription
tokenstringyesThe 6-digit code received by SMS.

Response 200 — Number verified.

FieldTypeDescription
messagestring
json
{
  "message": "User number verified"
}

Response 400token 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.

FieldTypeDescription
messagestring
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)

FieldTypeRequiredDescription
old_passwordstringyes
new_passwordstringyes
json
{
  "old_password": "correct horse battery staple",
  "new_password": "purple monkey dishwasher 42"
}

Response 200 — Updated.

FieldTypeDescription
messagestring
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)

FieldTypeRequiredDescription
new_passwordstringyes
current_passwordstringnoRequired only when a password already exists.
json
{
  "new_password": "purple monkey dishwasher 42"
}

Response 200 — Set.

FieldTypeDescription
messagestring
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)

FieldTypeRequiredDescription
emailstring (email)yes
recovery_codestringyes
new_passwordstringyes
json
{
  "email": "jane@acme.com",
  "recovery_code": "K7QF-2M9X-ZP4L-8HTW",
  "new_password": "purple monkey dishwasher 42"
}

Response 200 — Password reset.

FieldTypeDescription
messagestring
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)

FieldTypeRequiredDescription
recovery_codestringyes
json
{
  "recovery_code": "K7QF-2M9X-ZP4L-8HTW"
}

Response 200 — Sent.

FieldTypeDescription
messagestring
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

FieldTypeRequiredDescription
domainstringyesEmail domain (acme.com) or an email address (jane@acme.com).

Response 200 — Redirect the user to auth_url.

FieldTypeDescription
auth_urlstring (uri)
json
{
  "auth_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=…&state=…"
}

Response 400domain 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)

FieldTypeRequiredDescription
domainstringyes
client_idstringyesAzure AD application (client) id.
client_secretstringyes
organization_namestringnoDefaults to a name derived from the domain.
json
{
  "domain": "acme.com",
  "organization_name": "Acme Corp",
  "client_id": "6c3b0a1e-…",
  "client_secret": "…"
}

Response 200 — Saved.

FieldTypeDescription
messagestring
domainstring
organization_namestring
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)

FieldTypeRequiredDescription
codestringyesAuthorisation code from Microsoft.
statestringyesThe state Microsoft returned (carries the organisation domain).
json
{
  "code": "0.AXcA…",
  "state": "eyJkb21haW4iOiAiYWNtZS5jb20ifQ"
}

Response 200 — Signed in. Also sets the refresh_token cookie.

FieldTypeDescription
auth_tokenstring
expires_ininteger
userobjectThe signed-in user, as returned by GET /users/me/ (info) and SSO callbacks (user).
user.idinteger
user.first_namestring
user.last_namestring
user.usernamestring
user.emailstring (email)
user.phoneinteger, nullableMobile number as digits, or null.
user.llm_modelstringDisplay name of the user's default model ("" when unset).
user.languagestring ("en", "ar")UI language.
user.organizationstringOrganisation name ("" when the user has none).
user.last_loginstring (date-time), nullable
user.verifiedbooleanAccount approved (true for promo-code sign-ups, otherwise set by an admin).
user.email_verifiedboolean
user.use_cloudboolean
user.number_verifiedboolean
user.is_first_timebooleanTrue until the app's first-run flow has been acknowledged (POST /users/me/).
user.rolesstring[]Role names, e.g. ["General"].
user.is_staffbooleanDjango staff flag — what admin-only endpoints check.
user.dedicated_agentbooleanWhether the Dedicated Agent module is enabled for this user.
user.date_joinedstring (date-time)
user.is_ssobooleanTrue when the account has no usable password (SSO-only).
user.auth_methodsobjectWhich sign-in methods the account currently has.
user.auth_methods.passwordboolean
user.auth_methods.passkeyboolean
user.auth_methods.microsoftboolean
user.planobject, nullableActive subscription plan (hosted deployments only; null when there is none).
user.plan.namestring
user.plan.codestringStable plan key, e.g. pro.
user.plan.monthly_pricestring
user.plan.yearly_pricestring
user.plan.active_untilstring (date-time), nullable
user.plan.ownerbooleanPresent on organisation plans — true if this user owns the subscription.
user.entitlementsobjectLimits 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_codestringResolved plan key ("" on the free tier).
user.entitlements.plan_namestring
user.entitlements.owner_typestring ("organization", "user", "none")Who the quota pool belongs to.
user.entitlements.is_activeboolean
user.entitlements.seatsinteger
user.entitlements.support_tierstring
user.entitlements.featuresobjectCapability 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_runsinteger, nullable
user.entitlements.included_tokensinteger, nullable
user.entitlements.storage_bytesinteger, nullable
user.entitlements.run_history_daysinteger, nullable
user.entitlements.live_build_sessionsinteger, nullable
user.entitlements.workflow_runs_usedintegerRuns this calendar month.
user.entitlements.storage_used_bytesintegerLast crawl plus bytes reserved since.
user.entitlements.storage_breakdownobjectMeasured footprint from the last storage crawl.
user.entitlements.storage_breakdown.mediainteger
user.entitlements.storage_breakdown.vectorinteger
user.entitlements.storage_breakdown.workflowinteger
user.entitlements.storage_breakdown.totalinteger
user.entitlements.storage_breakdown.limitinteger, nullable
user.entitlements.storage_breakdown.measured_atstring (date-time), nullable
user.entitlements.enforcedbooleanWhether the feature gate is switched on for this deployment.
user.entitlements.storage_enforcedbooleanWhether exceeding storage_bytes actually blocks uploads.
user.plan_requiredbooleanTrue when the deployment requires an active plan and the app should send plan-less users to the plans page.
encryption_keystringThe 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_tokenstringMicrosoft 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)

FieldTypeRequiredDescription
codestringyes
statestringyes

Response 200 — Verified claims.

FieldTypeDescription
providerstring ("microsoft")
substringStable Microsoft account id.
emailstring
id_tokenstring
organizationstringThe user's (possibly updated) organisation name.
organization_changedboolean
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.

FieldTypeDescription
auth_urlstring (uri)
statestringSigned, 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)

FieldTypeRequiredDescription
codestringyes
statestringyesThe state from the authorize step.

Response 200 — Signed in. Also sets the refresh_token cookie.

FieldTypeDescription
auth_tokenstring
expires_ininteger
userobjectThe signed-in user, as returned by GET /users/me/ (info) and SSO callbacks (user).
user.idinteger
user.first_namestring
user.last_namestring
user.usernamestring
user.emailstring (email)
user.phoneinteger, nullableMobile number as digits, or null.
user.llm_modelstringDisplay name of the user's default model ("" when unset).
user.languagestring ("en", "ar")UI language.
user.organizationstringOrganisation name ("" when the user has none).
user.last_loginstring (date-time), nullable
user.verifiedbooleanAccount approved (true for promo-code sign-ups, otherwise set by an admin).
user.email_verifiedboolean
user.use_cloudboolean
user.number_verifiedboolean
user.is_first_timebooleanTrue until the app's first-run flow has been acknowledged (POST /users/me/).
user.rolesstring[]Role names, e.g. ["General"].
user.is_staffbooleanDjango staff flag — what admin-only endpoints check.
user.dedicated_agentbooleanWhether the Dedicated Agent module is enabled for this user.
user.date_joinedstring (date-time)
user.is_ssobooleanTrue when the account has no usable password (SSO-only).
user.auth_methodsobjectWhich sign-in methods the account currently has.
user.auth_methods.passwordboolean
user.auth_methods.passkeyboolean
user.auth_methods.microsoftboolean
user.planobject, nullableActive subscription plan (hosted deployments only; null when there is none).
user.plan.namestring
user.plan.codestringStable plan key, e.g. pro.
user.plan.monthly_pricestring
user.plan.yearly_pricestring
user.plan.active_untilstring (date-time), nullable
user.plan.ownerbooleanPresent on organisation plans — true if this user owns the subscription.
user.entitlementsobjectLimits 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_codestringResolved plan key ("" on the free tier).
user.entitlements.plan_namestring
user.entitlements.owner_typestring ("organization", "user", "none")Who the quota pool belongs to.
user.entitlements.is_activeboolean
user.entitlements.seatsinteger
user.entitlements.support_tierstring
user.entitlements.featuresobjectCapability 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_runsinteger, nullable
user.entitlements.included_tokensinteger, nullable
user.entitlements.storage_bytesinteger, nullable
user.entitlements.run_history_daysinteger, nullable
user.entitlements.live_build_sessionsinteger, nullable
user.entitlements.workflow_runs_usedintegerRuns this calendar month.
user.entitlements.storage_used_bytesintegerLast crawl plus bytes reserved since.
user.entitlements.storage_breakdownobjectMeasured footprint from the last storage crawl.
user.entitlements.storage_breakdown.mediainteger
user.entitlements.storage_breakdown.vectorinteger
user.entitlements.storage_breakdown.workflowinteger
user.entitlements.storage_breakdown.totalinteger
user.entitlements.storage_breakdown.limitinteger, nullable
user.entitlements.storage_breakdown.measured_atstring (date-time), nullable
user.entitlements.enforcedbooleanWhether the feature gate is switched on for this deployment.
user.entitlements.storage_enforcedbooleanWhether exceeding storage_bytes actually blocks uploads.
user.plan_requiredbooleanTrue when the deployment requires an active plan and the app should send plan-less users to the plans page.
id_tokenstringGoogle id token, returned once.
sso_rek_wrappedstring, nullableThe 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)

FieldTypeRequiredDescription
codestringyes
statestringyes

Response 200 — Verified claims.

FieldTypeDescription
providerstring ("google")
substring
emailstring
email_verifiedboolean
hdstringGoogle Workspace hosted domain ("" for consumer accounts).
id_tokenstring
organizationstring
organization_changedboolean
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.

FieldTypeDescription
optionsobjectPublicKeyCredentialCreationOptions (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)

FieldTypeRequiredDescription
credentialobjectyesA WebAuthn PublicKeyCredential as produced by the browser, JSON-serialised (navigator.credentials.get() / .create() output with base64url-encoded binary fields).
credential.idstringyesCredential id (base64url).
credential.rawIdstringnoSame as id.
credential.typestring ("public-key")yes
credential.responseobjectyesclientDataJSON plus, for registration, attestationObject and transports; for sign-in, authenticatorData, signature and userHandle.
namestringnoLabel shown in the passkey list. Default Passkey.
key_envelopeobjectnoClient-wrapped key material (opaque to the API).
rek_plaintextstringnoRecovery key, wrapped by the server at rest (web app only).
dek_plaintextstringnoData encryption key, wrapped by the server at rest (web app only).

Response 200 — Registered.

FieldTypeDescription
okboolean
passkey_debugobjectChallenge 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.

FieldTypeDescription
optionsobjectPublicKeyCredentialRequestOptions (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)

FieldTypeRequiredDescription
credentialobjectyesA WebAuthn PublicKeyCredential as produced by the browser, JSON-serialised (navigator.credentials.get() / .create() output with base64url-encoded binary fields).
credential.idstringyesCredential id (base64url).
credential.rawIdstringnoSame as id.
credential.typestring ("public-key")yes
credential.responseobjectyesclientDataJSON plus, for registration, attestationObject and transports; for sign-in, authenticatorData, signature and userHandle.

Response 200 — Signed in. Also sets the refresh_token cookie.

FieldTypeDescription
auth_tokenstring
expires_ininteger
absolute_expires_ininteger
usernamestringThe account's email (or username) so the app knows who signed in.
encryption_keystring, nullableData encryption key (hex), or null when the passkey has no envelope.
encryption_key_debugobjectWhere 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.

FieldTypeDescription
passkeysobject[]
passkeys[].credential_idstring
passkeys[].namestring
passkeys[].created_atstring (date-time)
passkeys[].last_used_atstring (date-time), nullable
limitinteger
can_addboolean
auth_method_countintegerPassword + 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)

FieldTypeRequiredDescription
credential_idstringnoOmit to remove every passkey.
json
{
  "credential_id": "pQECAyYgASFYIH…"
}

Response 200 — Removed.

FieldTypeDescription
okboolean
removedinteger
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…"}'

Finblade documentation