ClouisleClouisle

Authentication & Login API

User registration, click captcha, password login, TOTP 2FA, password reset, and email verification

The authentication endpoints cover account registration, click-based human verification, OAuth2-compatible password login, TOTP two-factor challenges and verification, password reset, and email verification. The base path is /api/v1.

TOTP enrollment and removal live on the separate /api/v1/totp/* endpoints (see the TOTP API), and SSO on /api/v1/sso/* (see the SSO API). This page focuses on the login chain itself and how it uses those two.

Endpoints Overview

MethodPathPurposeAuth
GET/api/v1/captchaGet a click-based human verification challengePublic
POST/api/v1/captcha/clickSubmit the click trace for a single-use proof tokenPublic
POST/api/v1/login/access-tokenSign in with username/email and passwordPublic
POST/api/v1/login/verify-totpExchange a temp token plus a TOTP code for an access tokenPublic (needs temp_token)
POST/api/v1/logoutLog out and blacklist the tokenBearer JWT
POST/api/v1/registerRegister a new accountPublic
POST/api/v1/send-verificationSend an email verification code/linkPublic
POST/api/v1/verify-emailVerify an email with a 6-digit codePublic
GET/api/v1/verifyVerify an email with the token from the email linkPublic
POST/api/v1/resend-verificationResend the registration verification emailPublic
POST/api/v1/forgot-passwordRequest a password reset emailPublic
POST/api/v1/reset-passwordSet a new password using a code or tokenPublic

Tokens and Sessions

A successful login returns a JWT to send as Authorization: Bearer <token>.

PropertyValue
LifetimeSite setting session_timeout_days, default 30 days (the in-code fallback is 7 days when the setting row is missing)
RefreshNo refresh token; sign in again once it expires
LogoutPOST /api/v1/logout blacklists the token, effective immediately
Single-session modeWith single_session = true, a new login invalidates the previous token, which then gets HTTP 401 / 2001
Logout without a tokenHTTP 401 / 2000

Human Verification (Captcha)

When enable_captcha is on (default false), registration, password login, and password recovery must carry a captcha proof.

1. Get a challenge

GET /api/v1/captcha
{
  "code": 0,
  "data": {
    "captcha_id": "c62040db-91fa-4ff8-9125-5e3681428256",
    "challenge": "{\"target_text\": \"Please click the red circle\", \"image\": \"data:image/png;base64,...\"}",
    "prompt": "captcha_click_prompt",
    "expires_in": 300
  },
  "msg": "success"
}

challenge is a JSON string you must parse yourself, and expires_in is 300 seconds.

2. Submit the click for a proof token

POST /api/v1/captcha/click
Content-Type: application/json

{
  "captcha_id": "c62040db-91fa-4ff8-9125-5e3681428256",
  "challenge": "{\"target_text\": \"Please click the red circle\", \"image\": \"data:image/png;base64,...\"}",
  "clicked_option": "red_circle",
  "elapsed_ms": 1340,
  "pointer": [
    {"x": 105.5, "y": 80.2, "t": 1200, "event": "move"},
    {"x": 108.0, "y": 82.0, "t": 1340, "event": "move"}
  ]
}
FieldTypeRequiredDescription
captcha_idstringYesThe ID returned by the challenge
challengestringYesEcho the challenge string verbatim
clicked_optionstringYesIdentifier of the option the user selected
elapsed_msintegerYesMilliseconds between rendering and the click
pointerarrayNoPointer trace points with x, y, t, and an optional event (default move); an empty array is accepted
{
  "code": 0,
  "data": {
    "captcha_id": "c62040db-91fa-4ff8-9125-5e3681428256",
    "captcha_token": "proof-token-string"
  },
  "msg": "success"
}

The proof is single-use: the same captcha_id + captcha_token cannot be exchanged for a second login.

HTTPCodeScenario
4005302Human verification required but captcha_id / captcha_token missing
4005303Proof invalid, already used, or expired; or the submitted trace failed validation

Password Login and Two-Factor Authentication

1. Submit credentials

Standard OAuth2 form encoding (application/x-www-form-urlencoded, not JSON):

POST /api/v1/login/access-token
Content-Type: application/x-www-form-urlencoded

username=alice@example.com&password=SecurePassword123!&captcha_id=c62040db-...&captcha_token=proof-token-string
Form fieldTypeRequiredDescription
usernamestringYesUsername or email; the backend field is identifier, received through an alias
passwordstringYesPassword
captcha_idstringRequired when captcha is onChallenge ID
captcha_tokenstringRequired when captcha is onClick proof
captcha_answerstringNoLegacy-client compatibility: used as the proof fallback when captcha_token is empty

The pre-login check order decides which error you see:

  1. Password login disabled by the deployment (sso_enabled = true with sso_allow_password_login = false) → 400 / 6306.
  2. Account locked → 400 / 5300, with data.lockout_seconds for the remaining wait.
  3. Wrong password → 400 / 2003, with data.remaining_attempts; reaching max_login_attempts (default 5) locks the account for lockout_duration_minutes (default 15) and returns 5300.
  4. Inactive or pending account → 400 / 2004 (a pending approval_status reports pending approval).
  5. TOTP enabled → returns requires_totp (below).
  6. Deployment requires TOTP (require_totp = true) but the user has not enrolled → returns requires_totp_setup.
  7. Email verification on (email_verification defaults to true) and the user unverified (superusers excepted) → 400 / 5004 with data.email.

Direct login success (200 OK)

{
  "code": 0,
  "data": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer" },
  "msg": "Login successful"
}

Two-factor challenge

When TOTP is enabled the server does not issue a full token; it returns a temp token valid for 5 minutes:

{
  "code": 0,
  "data": { "requires_totp": true, "temp_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." },
  "msg": "totp_required"
}

Enrollment required first

When the deployment sets require_totp and the account has not enrolled, the server returns a temp token valid for 30 minutes so the client can call /api/v1/totp/setup first:

{
  "code": 0,
  "data": { "requires_totp_setup": true, "temp_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." },
  "msg": "totp_setup_required"
}

All three are successful responses (code: 0), not errors. Clients must branch on the flags in data (requires_totp / requires_totp_setup / force_password_change) to decide the next screen.

Forced password change

When the password has expired or an administrator forced a change, a full token is still issued but flagged:

{
  "code": 0,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "bearer",
    "force_password_change": true,
    "reason": "expired"
  },
  "msg": "Login successful. You must change your password before continuing."
}

reason is expired (password expired) or force (administrator-forced). Send the user to POST /api/v1/users/me/change-password.

2. Verify the TOTP code and finish signing in

POST /api/v1/login/verify-totp
Content-Type: application/x-www-form-urlencoded

temp_token=temp-jwt-token-string&code=123456&is_backup_code=false
Form fieldTypeRequiredDefaultDescription
temp_tokenstringYes—The temp JWT from the previous step
codestringYes—The 6-digit authenticator code, or a backup recovery code when is_backup_code=true
is_backup_codebooleanNofalseWhether to validate code as a backup recovery code

Every field is a form field; sending a JSON body returns 422.

On success the forced-password-change rules are applied exactly as in login, and the response is:

{
  "code": 0,
  "data": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer" },
  "msg": "Two-factor authentication verified successfully"
}

Failure paths:

HTTPCodeScenario
4002001Temp token invalid or tampered with
4002002Temp token expired (totp_setup_expired)
4005313TOTP is not enabled for this account (or the secret is missing)
4005312Too many consecutive failures, temporarily throttled; data.seconds is the remaining wait
4005311The TOTP code or backup code is incorrect

A backup recovery code is consumed on first successful use, and the remaining count is written to the audit log.

3. Log out

POST /api/v1/logout
Authorization: Bearer YOUR_TOKEN

Blacklists the current token; the response has data: null and msg set to Logout successful. The client must sign in again afterwards.

Registration and Email Verification

1. Register a new account

POST /api/v1/register
Content-Type: application/json

{
  "username": "bob",
  "email": "bob@example.com",
  "password": "StrongPassword123!",
  "locale": "en",
  "terms_accepted": true,
  "captcha_id": "c62040db-...",
  "captcha_token": "proof-token-string"
}
FieldTypeRequiredDescription
usernamestringYesUnique username
emailstringYesUnique email
passwordstringYesPassword, validated against the password policy
localestringNoUI language
terms_acceptedbooleanRequired when the deployment asks for itMust be true when require_terms_acceptance_on_register is on, otherwise 1001
captcha_id / captcha_tokenstringRequired when captcha is onSee the captcha section

The outcome depends on site settings; msg and the account state change with them:

Scenarioapproval_statusis_activeemail_verifiedmsg
The very first userapprovedtruetrueRegistration successful. You are the first user and have been promoted to Super Admin!
require_approval = true (default)pendingfalseFollows the email-verification settingRegistration successful. Your account is pending admin approval.
require_approval = false with email_verification = true (default)approvedtruefalseRegistration successful. Please verify your email to activate your account.
Both disabledapprovedtruetrueRegistration successful

require_approval defaults to true, so registration is not immediately usable by default: the account needs an administrator approval (or completed email verification) before login, and otherwise login returns 2004 (pending) or 5004 (email unverified). The first user of the deployment skips all of that — active immediately, no email verification, and automatically granted the Super Admin role; later users get the default role and default team.

Failure paths: duplicate username 5002; duplicate email 5003; password outside the policy 1001 (data.errors.password lists the reasons); open registration disabled 5000; missing or failed captcha 5302/5303.

2. Send a verification email

POST /api/v1/send-verification
Content-Type: application/json

{ "email": "bob@example.com", "purpose": "register" }
FieldTypeRequiredDefaultDescription
emailstringYes—Recipient address
purposestringNoregisterEither register or reset_password

Behavior differs by purpose:

  • purpose = register: the user for that email must exist and be unverified; a missing user returns 400 / 4000 and an already-verified user returns 400 / 1001.
  • purpose = reset_password: always returns success to prevent enumeration; an email is sent when the user exists.

There is a 60-second cooldown per email address and purpose; a repeat inside it returns 400 / 5008 with data.remaining_seconds. When SMTP is disabled the response is 400 / 5007. Emails are sent by a background task, so the response only means “queued”.

3. Verify the email with the 6-digit code

POST /api/v1/verify-email
Content-Type: application/json

{ "email": "bob@example.com", "code": "839201", "purpose": "register" }
FieldTypeRequiredDefault
emailstringYes—
codestringYes—
purposestringNoregister

With purpose = register the user's email_verified is set to true.

{ "code": 0, "data": { "verified": true, "email": "bob@example.com" }, "msg": "Email verified successfully" }

An incorrect or already-consumed code returns 400 / 5005.

GET /api/v1/verify?token=email-verification-token-string

token is a query parameter. An invalid or expired token returns 400 / 5006; otherwise the response matches the code-based flow.

5. Resend the registration verification email

POST /api/v1/resend-verification
Content-Type: application/json

{ "email": "bob@example.com" }

Unlike send-verification with the register purpose, this endpoint also returns success for an unknown address (anti-enumeration), but an already-verified address returns 400 / 1001. The same 60-second cooldown (5008) and SMTP switch (5007) apply.

Password Recovery and Reset

1. Start password recovery

POST /api/v1/forgot-password
Content-Type: application/json

{ "email": "alice@example.com" }

To prevent email enumeration the endpoint always returns success regardless of whether the address exists (msg is If the email exists, a password reset link has been sent). Two cases still leak: SMTP disabled returns 400 / 5007, and a repeat inside the 60-second cooldown returns 400 / 5008. That is a deliberate availability-over-privacy tradeoff — plan for it.

2. Confirm the reset

Two mutually exclusive methods; provide at least one (neither gives 422):

POST /api/v1/reset-password
Content-Type: application/json

{ "email": "alice@example.com", "code": "839201", "new_password": "NewStrongPassword456!" }
POST /api/v1/reset-password
Content-Type: application/json

{ "token": "reset-token-string", "new_password": "NewStrongPassword456!" }
FieldTypeRequiredDescription
email + codestringOne of the two6-digit code method; the purpose is always reset_password
tokenstringOne of the twoEmail-link token method
new_passwordstringYesNew password, validated against the policy

A successful reset also clears the account's consecutive failure count and lockout time (failed_login_attempts, locked_until), so a locked-out user can recover immediately by resetting the password.

Failure paths: invalid code/token 400 / 5005; user not found 400 / 4000; new password outside the policy 400 / 1001.

Where TOTP and SSO Continue

The login flow only issues and verifies challenges; everything else lives on other pages:

NeedEndpointsDetails
Enroll TOTP, generate backup codes, check status, disablePOST /api/v1/totp/setup, /enable, /disable, /regenerate-backup-codes, GET /api/v1/totp/statusTOTP API
List public SSO providers, start SSO login, handle the callbackGET /api/v1/sso/providers, /sso/login/{provider_name}, /sso/callback/{provider_name}SSO API
Disconnect your own SSO connectionDELETE /api/v1/sso/connections/{connection_id}SSO API

Once a deployment enables SSO and disables password login (sso_allow_password_login = false), POST /api/v1/login/access-token returns 400 / 6306 outright. Clients should call GET /api/v1/sso/providers first and decide whether to render the password form at all.

Error Codes

HTTPCodeConstantScenario
4012000UNAUTHORIZEDLogout without a token
4002001INVALID_TOKENtemp_token invalid or tampered with
4002002TOKEN_EXPIREDTemp token expired (totp_setup_expired)
4002003INVALID_CREDENTIALSWrong username or password
4002004INACTIVE_USERAccount deactivated or pending approval
4005000REGISTRATION_DISABLEDOpen registration is disabled
4005002USERNAME_EXISTSUsername already taken
4005003EMAIL_EXISTSEmail already taken
4005004EMAIL_NOT_VERIFIEDEmail verification required before login
4005005VERIFICATION_CODE_INVALIDVerification code or reset token invalid
4005006VERIFICATION_CODE_EXPIREDVerification code or email token expired
4005007EMAIL_SEND_FAILEDSMTP not configured, email cannot be sent
4005008EMAIL_SEND_TOO_FREQUENTRepeat within the 60-second cooldown
4005300ACCOUNT_LOCKEDLocked after too many failures; data.lockout_seconds is the remaining wait
4005302CAPTCHA_REQUIREDCaptcha proof required
4005303CAPTCHA_INVALIDCaptcha proof invalid or already used
4005311TOTP_INVALIDTOTP code or backup code incorrect
4005312TOTP_RATE_LIMITEDTOTP failures throttled; data.seconds is the remaining wait
4005313TOTP_NOT_ENABLEDTOTP is not enabled for this account
4005315TOTP_SETUP_EXPIREDEnrollment session expired
4006306PASSWORD_LOGIN_DISABLEDDeployment enforces SSO and disabled password login

Security errors such as 5300, 5400, and 5312 all use HTTP 400 (only model quota 6103 uses 429). Always branch on the body code; see API Errors and Retries.

  • TOTP API — enrollment, backup codes, and status for two-factor authentication
  • SSO API — provider configuration, login redirect, and callbacks
  • Users API — change password, password status, and account deletion
  • API Quick Start — call an Agent directly with an API key

How is this guide?

On this page