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 |
| 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 |
| 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 |
|---|---|---|
| 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 |
|---|---|---|
| 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 |
| 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 |
|---|---|---|
| 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.
Related routes under /auth¶
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 |