Skip to content

Authentication resource

The Authentication resource covers signing up, signing in, completing a second factor, recovering an account, managing sessions, and moving a session to a new device. Every route on this page is below /auth, so Log in is POST https://tebhos.com/api/v1/auth/login.

Shared behaviour

Authentication defines the credential formats. Changing the credentials of an account that is already signed in is covered by Email and password changes, Multi-factor authentication, and Phone verification.

Authentication material is confidential and opaque

A client MUST treat every session token, MFA ticket, IP authorisation ticket, reset token, SSO state, and handoff code as a secret. It MUST NOT parse one, expose it to untrusted code, or write it to logs, analytics, or a URL the operation did not assign.

Most operations need no Authorization header, because the request carries its own proof: a password, a one-time code, an MFA ticket, a WebAuthn assertion, or a token delivered by email. The session routes, logout, the sudo routes, and complete desktop handoff read a user session token. Those routes are user-only: a bot token is rejected with 401.

A signed-in client sends its session token as Authorization: Bearer <token> (or the bare token). A session token has no expiry of its own: it stays valid until it is revoked by logout, by a session revocation route, or by a password reset. There is no refresh step and no ceiling on the number of live sessions.

Most unauthenticated routes key their rate limit bucket on the client IP address; the authenticated ones key it on the account. Every route is also subject to the global limit. A denial returns 429 RATE_LIMITED.

Error envelopes on this page

Most failures use the ordinary error response (error, code, message). The MFA completion routes, the session routes, and the WebAuthn routes report failures that belong to the account-security layer with a shorter body, where the code sits in error and there is no code member:

{"error": "MFA_TICKET_INVALID", "message": "Doğrulama oturumu zaman aşımına uğradı, tekrar giriş yapın"}
error Status Meaning
MFA_TICKET_INVALID 410 The MFA ticket is unknown, expired, or was destroyed after too many failed attempts
INVALID_CODE 401 The code is wrong, or the account's MFA attempt allowance is exhausted
CODE_EXPIRED 410 The SMS code expired
TOO_MANY_ATTEMPTS 429 Too many wrong SMS codes; request a new code
SESSION_NOT_FOUND 404 The session to revoke does not exist
VALIDATION 400 A body field failed validation (for example a code that is not 6 characters)

A client that handles both shapes reads code when present and falls back to error.

SSO enforcement

The instance can publish sso.enforced: true, but TEBHOS does not currently block local sign-in when it does. Clients that want to steer users to SSO read Get SSO status and hide the password form themselves.

Authentication token response object

A newly issued session token and a brief profile of the account. Log in, the MFA completions, Authenticate with WebAuthn, Reset a password, Revert an email change, and Complete SSO return this object.

Structure

Field Type Description
token string The newly issued user session token
profile profile brief object The signed-in account

A session token is returned once

A lost session token cannot be recovered through any operation. The account must sign in again to obtain a new session.

Branch on the presence of token

Log in and Reset a password return either this object or an MFA challenge response. Check for token (or mfa) to tell them apart.

Example

{
  "token": "<session token>",
  "profile": {
    "id": "1501314428688998182",
    "name": "Party Parrot",
    "username": "partyparrot",
    "email": "[email protected]",
    "image_url": "",
    "role": "user",
    "premium_type": 0
  }
}

Profile brief object

The account summary embedded in an authentication token response. Read the full account with Get current user.

Structure

Field Type Description
id snowflake The account ID
name string The display name
username ?string The username
email string The email address
image_url string A legacy avatar URL, usually an empty string. Resolve avatars from the user object instead
role string The account role
premium_type integer 0 free, 1 monthly, 2 yearly, 3 lifetime Premium
premium_until? string The ISO 8601 time Premium ends, omitted when there is none

MFA challenge response object

The ticket and method list a client needs to finish a login with a second factor. Log in and Reset a password return this object when the account has a second factor.

Structure

Field Type Description
mfa boolean Always true
ticket string The MFA ticket, valid for five minutes
methods MFA methods object Which second factors the account can use

Example

{
  "mfa": true,
  "ticket": "Zk8x0p2...",
  "methods": {"sms": false, "totp": true, "backup_codes": true, "webauthn": false}
}

MFA methods object

Structure

Field Type Description
totp boolean The account has an authenticator app; complete with TOTP
backup_codes boolean The account holds at least one unused backup code; complete with a backup code
webauthn boolean The account has at least one registered passkey; complete with WebAuthn
sms boolean The account has a verified phone as a second factor; complete with SMS

The ticket is destroyed after 5 failed attempts, and an account allows 10 failed MFA attempts per 15 minutes across all tickets. A successful completion consumes the ticket and clears both counters.

IP authorisation required error

Log in returns this 403 body when an account without a second factor signs in from an IP address it has not used before. No session is issued; the account receives an email with an authorisation link instead.

Structure

Field Type Description
code string IP_AUTHORIZATION_REQUIRED
message string Human-readable description
ip_authorization_required boolean Always true
ticket string The ticket used by Resend IP authorisation and Poll IP authorisation, valid for 15 minutes
email string The account's email address, so the client can say where the link was sent
resend_available_in integer Seconds until a resend is allowed, always 30

The body has no error member. The authorisation link in the email points at https://tebhos.com/authorize-ip#token=<token>, and the web app completes it with Authorise an IP address.

SSO status object

The public single sign-on state. Instance discovery embeds the same object.

Structure

Field Type Description
enabled boolean Whether SSO can be started. True only when the administrator enabled SSO and the provider has an authorisation URL, a token URL, a client ID, and a JWKS or user-info URL
enforced boolean Whether the administrator marked SSO as required. Always false while enabled is false
display_name ?string The configured provider name, or null
redirect_uri string The default callback URI, {webapp}/auth/sso/callback unless configured otherwise

SSO start object

Structure

Field Type Description
authorization_url string The provider authorisation URL, with state, nonce, and a PKCE S256 challenge. Send the user there unchanged
state string The one-use state, valid for ten minutes
redirect_uri string The callback URI bound to this state

Authentication session object

One live session of the authenticated account.

Structure

Field Type Description
id string The session identifier accepted by Revoke a session and Log out sessions: the SHA-256 digest of the session token in hexadecimal
id_hash ?string The same digest encoded as base64url
current boolean Whether this session supplied the token of the current request
client_info client info object Device metadata parsed from the recorded user agent
masked_ip ?string The recorded IP address, partly redacted
location ?string The approximate location as a City, Region, Country label, or null
user_agent ?string The recorded User-Agent
created_at ISO8601 timestamp When the session was created
used_at ?ISO8601 timestamp The approximate time of last use
approx_last_used_at ?ISO8601 timestamp The same value as used_at, with millisecond precision
expires_at ?ISO8601 timestamp Always null for current sessions, which do not expire
scylla_id ?string Internal storage key of the session; ignore it

A client that needs its own session reads the entry whose current is true.

Client info object

Device metadata inferred from a User-Agent. Treat every value as a display label.

Structure

Field Type Description
platform ?string A label such as Chrome, Firefox, or TEBHOS Windows for the desktop app
os ?string Windows, macOS, Linux, Android, or iOS, or null when unknown
browser ?string The browser name, or null for the desktop app and native clients
device string mobile or desktop
location ?object {city, region, country}, each a nullable string, or null when unknown

Handoff initiation object

Structure

Field Type Description
code string The handoff code to show the user: 12 characters from ABCDEFGHJKMNPQRSTUVWXYZ23456789, formatted as two groups of six separated by a hyphen
expires_at ISO8601 timestamp When the handoff expires, five minutes after creation

Handoff information object

Structure

Field Type Description
status string pending or expired
client_info ?object {platform, os, location} describing the initiating device, or null when expired. platform is TEBHOS Desktop for the desktop app

Handoff status object

Structure

Field Type Description
status string pending, expired, or completed
token? string The new session token, present only when completed
user_id? snowflake The account ID, present only when completed
user? ?partial user object The account, present only when completed

Username availability object

Structure

Field Type Description
available boolean Whether the value can be used
reason string Why it cannot be used, or an empty string when it can

Sudo grant object

Structure

Field Type Description
granted_at ISO8601 timestamp When sudo mode was granted
expires_at ISO8601 timestamp When it lapses, five minutes later

Registration

Registration is a two-step flow. Send a verification code emails a six-digit code to the address (after CAPTCHA), then Register an account submits the form with that code. A valid code proves both control of the address and that the CAPTCHA was passed.

Check username availability

POST /auth/check-username

Checks whether a username is well formed and free. Authentication is not required. Returns a username availability object; an unusable value still returns 200.

JSON body

Field Type Description
username string The username to check. It is lowercased before checking

A username is 4-32 characters of lowercase ASCII letters, digits, _, and .. everyone, here, and any value containing tebhos or system message are reserved.

Rate limit

20 requests per minute for each IP address, on the auth:check-username bucket.

Check email availability

POST /auth/check-email

Checks whether an email address is free. Authentication is not required. Returns a username availability object.

JSON body

Field Type Description
email string The address to check (at most 254 characters). It is trimmed and lowercased

This route discloses registration

available: false with a "already in use" reason tells the caller the address has an account. Request password recovery and Log in do not disclose this.

Rate limit

20 requests per minute for each IP address, on the auth:check-email bucket.

Get username suggestions

POST /auth/username-suggestions

Derives a username candidate from a display name. Authentication is not required. Returns {"suggestions": [...]} with at most one entry.

JSON body

Field Type Description
global_name string The display name to derive from

Letters are transliterated to ASCII (Turkish and common accented letters included), spaces, -, and . become _, other characters are dropped, and the result is cut to 32 characters. The array is empty when the result is not a valid username. The suggestion is not reserved and may be taken before it is used.

Rate limit

10 requests per 10 seconds for each IP address, on the auth:register bucket, shared with Register an account.

Send a verification code

POST /auth/send-verification

Emails a six-digit registration code to an address that has no account yet. Authentication is not required.

The route is CAPTCHA-gated: when the instance has a CAPTCHA provider, a missing X-Captcha-Token returns 400 CAPTCHA_REQUIRED and a rejected one returns 400 INVALID_CAPTCHA. See CAPTCHA handling.

Request headers

Field Type Description
X-Captcha-Token? string The CAPTCHA solution
X-Captcha-Type? string hcaptcha or turnstile; omit to use the instance provider

JSON body

Field Type Description
email string The address to verify
name? string The display name, used to greet the user in the email
username? string The username the user typed. When present it is checked for format and availability now, so the form can report a problem before the code is sent

The address is checked before sending: its domain must have usable DNS records, it must not be on the instance's banned or disposable-domain lists (400 INVALID_FORM_BODY), and it must not already have an account (409 CONFLICT). A supplied username that is taken also returns 409 CONFLICT.

Response

Status Body Condition
200 {"message": string, "expires_in": 300} The code was generated and sent
400 error response CAPTCHA, email format, email domain, or username format is invalid
409 error response The email or username is already in use
429 error response A bucket denied the request

Side effects

The code is valid for five minutes and replaces any earlier code for the same address. The response does not confirm delivery.

Rate limit

3 requests per 3 minutes for each IP address, on the auth:send-verification bucket, and 1 request per minute for each email address, on the auth:send-verification-email bucket.

Register an account

POST /auth/register

Creates an account and signs it in. Authentication is not required. Returns 201 with the new session.

Request headers

Field Type Description
Accept-Language? string Selects the locale stored on the new account

JSON body

Field Type Description
name string The display name (2-32 characters)
username string The username. It is lowercased, then must be 4-32 characters of lowercase ASCII letters, digits, _, and ., and must not be reserved
email string The account email address
password string The password (8-256 characters)
birth_date string The date of birth as YYYY-MM-DD. The account holder must be at least 13 years old
verification_code string The six-digit code from Send a verification code

The code is single-use and bound to the email address. A missing, wrong, or expired code returns 400 INVALID_FORM_BODY. The email checks of Send a verification code run again, so an address or username taken in the meantime returns 409 CONFLICT.

Response

Status Body Condition
201 registration response The account was created and signed in
400 error response A field, the code, the date of birth, or the minimum age is invalid
409 error response The email or username is already in use
429 error response A bucket denied the request

Registration response

Field Type Description
message string A confirmation message
username string The stored (lowercased) username
token string The new session token
profile object {id, name, username, image_url}, where image_url is null

Side effects

The account is created active, with the registering IP address pre-authorised (so the first login from it does not need IP authorisation). Privacy defaults depend on age: accounts under 18 start with stricter friend-request and sensitive-content settings. When the instance has an auto-join invite configured, the new account joins that guild before the response is returned.

Registration is not idempotent

There is no retry key. A client that loses the response signs in with Log in instead of registering again; the second registration would fail on the duplicate email.

Rate limit

10 requests per 10 seconds for each IP address, on the auth:register bucket. In addition, each email address allows 3 attempts per 15 minutes (registration:email), each IP address 3 per hour (registration:ip), and each IPv4 /24 or IPv6 /48 network 15 per hour (registration:subnet).

Log in

POST /auth/login

Validates an email address or username and a password. Authentication is not required. Returns an authentication token response when no second factor is needed, or an MFA challenge response when the account has one.

The route is CAPTCHA-gated in the same way as Send a verification code.

Request headers

Field Type Description
X-Captcha-Token? string The CAPTCHA solution
X-Captcha-Type? string hcaptcha or turnstile; omit to use the instance provider
User-Agent? string Recorded on the new session and shown by List authentication sessions

JSON body

Field Type Description
login string The email address or the username. A value containing @ is treated as an email address
password string The password
remember_me? boolean Accepted for compatibility and ignored; sessions never expire on their own

Unknown accounts and wrong passwords are indistinguishable

An unknown email or username, a wrong password, and an account that has no password (for example one created through SSO) all return the same 401 UNAUTHORIZED.

An account awaiting approval, a rejected registration, a suspended account, and an account under a live temporary suspension return 403 FORBIDDEN. A temporary suspension that has already ended is lifted by the successful login.

A new client IP address on an account without a second factor returns 403 with the IP authorisation required error. An account that has a second factor never goes through IP authorisation.

A correct password reactivates the account

On an account the user had disabled, a correct password clears the disabled state. It also cancels a scheduled account deletion. Both happen before the second factor is requested, without any extra confirmation.

Response

Status Body Condition
200 authentication token response or MFA challenge response The credentials were accepted
400 error response The body or the CAPTCHA is invalid
401 error response The credentials are wrong
403 error response or IP authorisation required error Account state or IP authorisation prevents the login
429 error response A bucket or the failed-login lockout denied the request

Side effects

A login without a second factor creates one session. A login with a second factor issues a five-minute MFA ticket and no session.

Rate limit

10 requests per 10 seconds for each IP address, on the auth:login bucket. In addition, each login identifier allows 5 attempts per 15 minutes (auth:login:email) and each IP address 10 attempts per 30 minutes (login:ip); every attempt counts, successful or not. Separately, 5 failed logins from one IP address lock it out for up to 15 minutes, and 10 for an hour.

Example

curl -X POST https://tebhos.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"login": "[email protected]", "password": "correct horse battery staple"}'

Complete login with TOTP

POST /auth/login/mfa/totp

Consumes an MFA ticket with an authenticator app code and creates the session. Authentication is not required. Returns an authentication token response.

JSON body

Field Type Description
ticket string The ticket from the MFA challenge response
code string The six-digit authenticator code

A wrong code returns 401 INVALID_CODE and counts against the attempt allowances. An unknown or expired ticket returns 410 MFA_TICKET_INVALID. Backup codes are not accepted here; use Complete login with a backup code.

Rate limit

5 requests per minute for each IP address, on the auth:login:mfa bucket, shared by every MFA login route.

Complete login with a backup code

POST /auth/login/mfa/backup-code

Consumes an MFA ticket with one of the account's backup codes. Authentication is not required. Returns an authentication token response.

JSON body

Field Type Description
ticket string The ticket from the MFA challenge response
code string An unused backup code (8 or 9 characters)

The accepted backup code is consumed permanently. Errors are the same as Complete login with TOTP.

Rate limit

5 requests per minute for each IP address, on the auth:login:mfa bucket.

Send an SMS login code

POST /auth/login/mfa/sms/send

Sends a login code by SMS to the account's verified phone. Authentication is not required. Returns {"message": string}.

Requires SMS support

SMS delivery works only on an instance that publishes features.sms_enabled: true in instance discovery.

JSON body

Field Type Description
ticket string The ticket from the MFA challenge response

Rate limit

3 requests per minute for each IP address, on the auth:login:mfa:sms:send bucket.

Complete login with SMS

POST /auth/login/mfa/sms

Consumes an MFA ticket with the SMS code. Authentication is not required. Returns an authentication token response.

JSON body

Field Type Description
ticket string The ticket from the MFA challenge response
code string The six-digit SMS code

Rate limit

5 requests per minute for each IP address, on the auth:login:mfa bucket.

Get WebAuthn MFA options

POST /auth/login/mfa/webauthn/authentication-options

Creates a passkey challenge for an MFA ticket. Authentication is not required. The ticket is read but not consumed.

JSON body

Field Type Description
ticket string The ticket from the MFA challenge response

Response

Status Body Condition
200 WebAuthn options object A challenge was created
400 error response The account has no registered passkey
410 MFA_TICKET_INVALID The ticket is unknown or expired

The challenge is valid for five minutes and is bound to the ticket.

Rate limit

5 requests per minute for each IP address, on the auth:login:mfa bucket.

WebAuthn options object

The challenge a browser passes to navigator.credentials.get().

Structure

Field Type Description
challenge string The base64url challenge
rp_id string The relying party ID, the web app's host name (for example tebhos.com)
allow_credential_ids? array[string] The IDs of the account's registered passkeys (MFA and sudo options only)
user_verification? string required (discoverable options only)

WebAuthn assertion fields

The WebAuthn completion routes take the browser's assertion as flat base64url fields:

Field Type Description
credential_id string The credential ID
client_data_json string The base64url clientDataJSON. Its type must be webauthn.get and its origin must be the TEBHOS web app origin
authenticator_data string The base64url authenticator data
signature string The base64url signature

Complete login with WebAuthn MFA

POST /auth/login/mfa/webauthn

Verifies a passkey assertion against the challenge from Get WebAuthn MFA options, consumes the ticket, and creates the session. Authentication is not required. Returns an authentication token response.

JSON body

Field Type Description
ticket string The ticket from the MFA challenge response
credential_id string See WebAuthn assertion fields
client_data_json string See WebAuthn assertion fields
authenticator_data string See WebAuthn assertion fields
signature string See WebAuthn assertion fields

A challenge or origin mismatch returns 400, an unknown credential 404, and a failed signature 401. The authenticator's signature counter must advance.

Rate limit

5 requests per minute for each IP address, on the auth:login:mfa bucket.

Get discoverable WebAuthn options

POST /auth/webauthn/authentication-options

Creates a challenge for passwordless sign-in with a discoverable passkey. Authentication is not required and the request has no body. Returns a WebAuthn options object with user_verification: "required" and no credential list.

The challenge is valid for five minutes and can only be used by Authenticate with WebAuthn.

Rate limit

20 requests per 10 seconds for each IP address, on the auth:webauthn:options bucket.

Authenticate with WebAuthn

POST /auth/webauthn/authenticate

Signs in with a discoverable passkey. The account is resolved from the credential ID. Authentication is not required. Returns an authentication token response.

JSON body

Field Type Description
challenge string The challenge from Get discoverable WebAuthn options
credential_id string See WebAuthn assertion fields
client_data_json string See WebAuthn assertion fields
authenticator_data string See WebAuthn assertion fields
signature string See WebAuthn assertion fields

The authenticator must report user verification. An unknown credential, a challenge mismatch, missing user verification, or a failed signature returns 401. The passkey is the whole proof, so no second factor is requested. An account that is not active returns 403 FORBIDDEN.

Rate limit

10 requests per 10 seconds for each IP address, on the auth:webauthn:authenticate bucket.

Get SSO status

GET /auth/sso/status

Reads the public single sign-on state. Authentication is not required. Returns an SSO status object.

Rate limit

10 requests per 10 seconds for each IP address, on the auth:sso:start bucket, shared with Start SSO.

Start SSO

POST /auth/sso/start

Starts an OpenID Connect sign-in. Authentication is not required. Returns an SSO start object.

JSON body

Field Type Description
redirect_to? ?string The in-app path to return to after sign-in (at most 2048 characters). It is kept only when it starts with a single / and contains no line break
redirect_uri? ?string The provider callback URI (at most 2048 characters). Only the default from Get SSO status and the mobile callback tebhos://auth/sso/callback are accepted

Response

Status Body Condition
200 SSO start object The state was created
400 error response SSO is not enabled or not fully configured, or a field is invalid
429 error response The route bucket denied the request

Rate limit

10 requests per 10 seconds for each IP address, on the auth:sso:start bucket.

Complete SSO

POST /auth/sso/complete

Exchanges the provider's authorisation code, verifies the identity, and signs in the linked account. Authentication is not required. Returns an authentication token response with one extra member, redirect_to (the sanitised path bound at Start SSO, or an empty string).

JSON body

Field Type Description
code string The authorisation code returned by the provider
state string The state from Start SSO

The state is consumed on first use, so a repeated request fails with 400. The provider must return a verified email address.

A provider identity is linked to exactly one TEBHOS account. An identity seen for the first time is linked to the existing account with the same email address. An identity that matches no account returns 403 FORBIDDEN: TEBHOS does not create accounts through SSO, so the user registers first and then signs in with SSO.

A verified provider email adopts the local account

When the identity is not yet linked, TEBHOS links it to the account holding the same email address. An administrator MUST enable SSO only for a provider that controls the addresses it asserts.

SSO sign-in never asks for a second factor.

Rate limit

15 requests per 10 seconds for each IP address, on the auth:sso:complete bucket.

Log out

POST /auth/logout

Revokes the session whose token is in the Authorization header. The request has no body. Returns 204.

The account's other sessions survive. Use Log out other sessions or Log out sessions to end them.

Response

Status Body Condition
204 empty The session was revoked
401 error response The header is missing
429 error response The route bucket denied the request

The revoked token stops authenticating immediately.

Rate limit

20 requests per 10 seconds for each client, on the auth:logout bucket.

List authentication sessions

GET /auth/sessions

Lists every live session of the authenticated account. Requires a user session token. Returns {"sessions": [...]}, an object whose sessions member is an array of authentication session objects.

Rate limit

40 requests per 10 seconds for each account, on the auth:sessions bucket.

Revoke a session

DELETE /auth/sessions/{session_id}

Revokes one session. Requires a user session token. Returns {"message": string}.

Path parameters

Field Type Description
session_id string The id of an authentication session (64 hexadecimal characters)

Query parameters

Field Type Description
silent? boolean When true, the account's other devices are not notified of the change (default false)

An id that is not a session of the account returns 404 SESSION_NOT_FOUND, and a value in the wrong format returns 400. The revoked session's Gateway connection is closed.

Rate limit

20 requests per 10 seconds for each account, on the auth:sessions:logout bucket, shared by every session revocation route.

Log out other sessions

DELETE /auth/sessions/others

Revokes every session of the account except the one making the request. Requires a user session token. An empty JSON body ({}) is expected. Returns {"revoked_count": integer, "message": string}.

Rate limit

20 requests per 10 seconds for each account, on the auth:sessions:logout bucket.

Log out all sessions

DELETE /auth/sessions

Revokes every session of the account, including the one making the request. Requires a user session token. Returns {"revoked_count": integer, "message": string}.

Rate limit

20 requests per 10 seconds for each account, on the auth:sessions:logout bucket.

Log out sessions

POST /auth/sessions/logout

Revokes the listed sessions. Requires a user session token and sudo mode. Returns 204.

JSON body

Field Type Description
session_ids array[string] The id values of the sessions to revoke (1-100 entries)
password? string Sudo proof for an account without a second factor
mfa_code? string Sudo proof: an authenticator code or a backup code
sudo_proof? string Sudo proof issued by a passkey assertion

The proof fields are the sudo verification fields. A password is refused on an account that has a second factor (403 SUDO_MODE_REQUIRED); a missing proof also returns 403 SUDO_MODE_REQUIRED. A wrong password or code returns 401.

An ID that is malformed or names no session of the account is skipped. The request can revoke the calling session; a client that wants to keep its session omits the entry whose current is true.

Rate limit

20 requests per 10 seconds for each account, on the auth:sessions:logout bucket.

Request password recovery

POST /auth/forgot-password

Emails a password reset link to an address. Authentication is not required. Returns 200 with {"message": string} whether or not the address has an account.

JSON body

Field Type Description
email string The address that receives the reset link

An address whose domain has no usable DNS records returns 400. Any other address gets the same 200 response, so the response never reveals whether an account exists.

Side effects

For an address with an account, TEBHOS sends a link to https://tebhos.com/reset-password?token=<token>. The reset token is valid for 15 minutes.

Rate limit

5 requests per minute for each IP address, on the auth:forgot bucket. In addition, each IP address allows 20 requests (password_reset:ip) and each email address 5 requests (password_reset:email) per 30 minutes.

Validate a password reset token

GET /auth/reset-password/verify

Checks a reset token without consuming it. Authentication is not required.

Query parameters

Field Type Description
token string The reset token from the email link

Response

Status Body Condition
200 {"valid": boolean, "expires_in": integer} The token was checked. expires_in is the remaining lifetime in seconds, or 0 when the token is not valid

Rate limit

20 requests per minute for each IP address, on the auth:reset:validate bucket.

Reset a password

POST /auth/reset-password

Consumes a reset token, sets a new password, and revokes every existing session. Authentication is not required. Returns an authentication token response when the account has no second factor, or an MFA challenge response when it has one.

JSON body

Field Type Description
token string The reset token from the email link
new_password string The new password (8-256 characters)

An unknown, used, or expired token returns 400 INVALID_FORM_BODY.

Response

The token response has four extra members:

Field Type Description
user_id snowflake The account ID
message string A confirmation message
all_sessions_revoked boolean Always true
revoked_count integer How many sessions were revoked

Resetting a password revokes every session

Every session of the account ends, including those on other devices. The response then issues one new session, or an MFA ticket when the account has a second factor.

Rate limit

10 requests per minute for each IP address, on the auth:reset bucket.

Verify an email address

POST /auth/verify

Consumes an email verification token and marks the account's address verified. Authentication is not required. Returns 204.

JSON body

Field Type Description
token string The verification token

An unknown token returns 400; a bot account returns 403. On success the account's own sessions receive a user update.

Registration already verifies the address

New accounts prove their address with the code from Send a verification code, so TEBHOS does not currently email verification tokens for this route.

Rate limit

10 requests per minute for each IP address, on the auth:verify bucket.

Revert an email change

POST /auth/email-revert

Consumes an email revert token, restores the previous address, sets a new password, and clears every other credential. Authentication is not required. Returns an authentication token response.

JSON body

Field Type Description
token string The revert token
password string The new password (8-128 characters)

Reversion destroys every other credential

The account's sessions, authenticator app, passkeys, backup codes, and authorised IP addresses are all removed. Only the requesting IP address stays authorised, and the response issues one new session.

An unknown token returns 400, a bot account 403, and an account under a live temporary suspension 403.

Not currently issued

TEBHOS's email change flow does not currently send revert tokens, so this route is only reachable with a token issued by an administrator tool.

Rate limit

10 requests per minute for each IP address, on the auth:email_revert bucket.

Authorise an IP address

POST /auth/authorize-ip

Consumes the token from an IP authorisation email, authorises the pending IP address, and completes the held login. Authentication is not required. Returns 204.

JSON body

Field Type Description
token string The token from the authorisation link

An unknown, used, or expired token returns 400, an account that no longer exists 404, and a bot account 403.

Side effects

The pending IP address is added to the account's authorised set, and one session is created with the IP address and user agent of the held login attempt. The caller of this route receives no token: the new token is published against the ticket for 60 seconds and read by the waiting device through Poll IP authorisation.

Rate limit

5 requests per minute for each IP address, on the auth:authorize_ip bucket.

Resend IP authorisation

POST /auth/ip-authorization/resend

Sends the authorisation email for a held login again. Authentication is not required. Returns 204.

JSON body

Field Type Description
ticket string The ticket from the IP authorisation required error

Each ticket allows one resend, no earlier than 30 seconds after the ticket was issued. An early resend returns 429 RATE_LIMITED with Retry-After set to the remaining seconds. A second resend, or an unknown or expired ticket, returns 400. The resend reuses the same link, so an earlier email stays valid.

Rate limit

5 requests per minute for each IP address, on the auth:ip_authorization_resend bucket.

Poll IP authorisation

GET /auth/ip-authorization/poll

Reads the result of a held login. Authentication is not required.

Query parameters

Field Type Description
ticket string The ticket from the IP authorisation required error

Response

Status Body Condition
200 {"completed": false} The link has not been used yet
200 {"completed": true, "token", "user_id", "user"} The link was used; token is the new session token and user is {id, username, discriminator, global_name, avatar, bot}
400 error response The ticket is unknown or expired, or the 60-second result window has passed

Reading the result does not consume it

Repeated polls return the same token for 60 seconds, so anyone holding the ticket in that window can read it.

Rate limit

60 requests per minute for each IP address, on the auth:ip_authorization_poll bucket.

Initiate desktop handoff

POST /auth/handoff/initiate

Creates a pending handoff so that a signed-in device (for example the web app) can sign in another device (for example the desktop app). Authentication is not required and the request has no body. Returns a handoff initiation object.

Request headers

Field Type Description
User-Agent? string Shown to the approving device
X-Tebhos-Platform? string desktop for the TEBHOS desktop app

The initiating device shows the code to the user, who enters it on a signed-in device. The initiating device then polls Get desktop handoff status.

Rate limit

10 requests per minute for each client, on the auth:handoff:initiate bucket.

Get desktop handoff information

GET /auth/handoff/{code}/info

Describes the device that started a handoff, so the approving device can show it before approving. Authentication is not required. Returns a handoff information object.

Path parameters

Field Type Description
code string The handoff code. Hyphens and whitespace are removed and the rest is upper-cased before lookup

A code that is not 12 characters from the handoff alphabet returns 400. An unknown or expired code returns 200 with status: "expired" and counts as a failed attempt for the caller's IP address; 5 failed attempts in 15 minutes block that IP address (400). Each code can be looked up at most three times.

Looking up the code makes it completable

Complete desktop handoff accepts only a code that was looked up here first. An approving client MUST show the device information and get explicit confirmation before completing.

Rate limit

10 requests per minute for each client, on the auth:handoff:info bucket.

Complete desktop handoff

POST /auth/handoff/complete

Approves a handoff: creates a new session for the initiating device on the authority of the approving device's session. Returns 204.

JSON body

Field Type Description
code string The handoff code
user_id snowflake The account the approving session belongs to
token? string The approving device's session token. When omitted, the token is read from Authorization: Bearer <token>

Prefer the header

Sending token in the body puts a live credential in a JSON payload. Use the Authorization header and omit token.

A missing or invalid approving token, or a user_id that does not match it, returns 401. An inactive account returns 403. A code that was never looked up, is unknown, or has expired returns 400.

Side effects

The new session is created for the approving account and records the IP address and User-Agent of the completion request. The approving session stays valid. The token is held against the code until the handoff's five minutes run out, and is delivered once by Get desktop handoff status.

Rate limit

10 requests per minute for each client, on the auth:handoff:complete bucket, plus the failed-attempt block described under Get desktop handoff information.

Get desktop handoff status

GET /auth/handoff/{code}/status

Reports the state of a handoff to the device that started it. Authentication is not required. Returns a handoff status object.

Path parameters

Field Type Description
code string The handoff code, normalised as for Get desktop handoff information

The completed status is delivered exactly once

The first poll after completion returns completed with the token and deletes it. Later polls report expired, so the initiating device MUST keep the token when it first receives it. The code is the only credential this route checks, so keep it private.

A malformed code returns 400.

Rate limit

60 requests per minute for each client, on the auth:handoff:status bucket.

Cancel a desktop handoff

DELETE /auth/handoff/{code}

Discards a handoff and anything stored against its code. Authentication is not required. Returns 204, also for a code that does not exist.

A session already created by Complete desktop handoff is not revoked. A malformed code returns 400.

Rate limit

10 requests per minute for each client, on the auth:handoff:cancel bucket.

Grant sudo mode

POST /auth/sudo

Proves the user's identity again and grants sudo mode to the calling session for five minutes. Sensitive routes that accept a grant then need no proof in their body. Requires a user session token. Returns a sudo grant object.

JSON body

Field Type Description
password? string The password, accepted only on an account without a second factor
mfa_code? string An authenticator code or a backup code
sudo_proof? string A proof issued by a passkey assertion

The grant belongs to the session that requested it, not to the account.

Rate limit

5 requests per 10 seconds for each IP address, on the auth:sudo bucket. Code and password proofs also have their own per-account limits (10 per 15 minutes and 10 per 5 minutes).

Revoke sudo mode

DELETE /auth/sudo

Ends the calling session's sudo grant early. Requires a user session token. Returns 204.

List sudo methods

GET /auth/sudo/mfa-methods

Returns which proofs the account can use for sudo mode: {"totp": boolean, "webauthn": boolean, "has_mfa": boolean}. Requires a user session token.

Rate limit

20 requests per minute for each account, on the sudo:mfa:methods bucket.

Get sudo WebAuthn options

POST /auth/sudo/webauthn/authentication-options

Creates a passkey challenge for sudo mode. Requires a user session token. Returns a WebAuthn options object with allow_credential_ids. An account with no passkey returns 400. Multi-factor authentication describes how the assertion is turned into a sudo_proof.

Rate limit

10 requests per minute for each account, on the sudo:webauthn:options bucket.

Resolve a token

GET /auth/whoami

Resolves the credential in the Authorization header. Accepts a user session token or a bot token. Returns {"id": snowflake, "bot": boolean}, where id is the user or bot user ID.

An invalid credential returns 401.

These routes live under /auth but are documented with the resource they change:

Route Documented on
GET /auth/me Current user
/auth/password-change/*, /auth/email-change/* Email and password changes
/auth/passkeys*, /auth/2fa/* Multi-factor authentication
/auth/me/privacy, /auth/me/notifications, /auth/me/appearance, /auth/me/language User settings
/auth/me/subscription Premium
/auth/me/gifts Gift inventory
POST /auth/account/disable, POST /auth/account/delete Current user