Authentication

40 endpoints, generated from the API's own OpenAPI document. Sign-up, sign-in, verification, password reset and the second factor (better-auth). These answer `{code, message}` JSON, not problem documents, except where noted.

POST /v2/auth/sign-up/email

Create an account with email and password (Turnstile-guarded; a verification mail is sent)

Credentials: none. Operation id authSignUpEmail.

Anonymous. Creates the better-auth user, its credential, the Taigan account and the person's own organization (named from orgName, else from the address), records acceptance of the Terms and Privacy Policy versions in force (submitting the form is the acceptance; a legal.accepted audit row is written), stores the marketing choice (opted out unless marketingOptIn is true), and mails a verification link. No session is created: sign-in is refused until the address is verified (the link signs the person in). Requires a Cloudflare Turnstile token in the x-captcha-response header, checked by Taigan before better-auth sees the request (the token is read from the header only). Anti-enumeration: signing up with an address that already has a user answers the same 200 with a synthetic user and sends nothing; if that existing user is still UNVERIFIED, its stored password is discarded (AUTH-2), and when the real owner later verifies they are signed in and sent a password-reset link to choose one. Accepts application/json or application/x-www-form-urlencoded. Not the invitation sign-up: that is POST /v2/invites/sign-up.

Parameters

x-captcha-responseheader · stringrequired
The Cloudflare Turnstile widget token for this form submission.

Request body

Required; application/json.

namestringrequired
The person's name (becomes the account's display name).
emailstring (email)required
The sign-in address. Stored lower-cased.
passwordstringrequired
The password: at least 10 and at most 128 characters. (at most 128 characters)
orgNamestring
Name for the person's own organization. Optional; read once at creation (an auto name is used when absent).
marketingOptInboolean
True if the person ticked the marketing-consent box. Absent or false means opted out (O26 R2). (default false)
callbackURLstring
Where the verification link sends the browser after verifying (relative, or on a trusted origin). Defaults to /.
imagestring
A profile image URL. Stored, never shown.
rememberMeboolean
Accepted for compatibility; has no effect here because sign-up never creates a session.

Responses

200 success

Accepted. The same body whether or not the address was new.

As application/json.

tokennullalways present
Always null: no session is created until the address is verified.
userAuthUseralways present
The better-auth user (the identity behind a Taigan account; one to one with accounts). Timestamps are ISO 8601 strings.
user.idstring (uuid)always present
The auth user's id (auth_users.id). Not the Taigan account id that /v2/account reports.
user.namestringalways present
The name given at sign-up or set through POST /v2/auth/update-user. Mirrored into the account's display_name.
user.emailstring (email)always present
The sign-in address, lower-cased.
user.emailVerifiedbooleanalways present
Whether the address has been proved by its verification link. Sign-in is refused until it is true.
user.imagestring or null
A profile image URL. Stored if supplied; Taigan and Kelpie never show it.
user.createdAtstring (date-time)always present
When the user was created.
user.updatedAtstring (date-time)always present
When the user was last changed.
user.orgNamestring or null
The organization name typed on the sign-up form, used once to name the person's own organization. Never read again; the organization's real name is on /v2/account.
user.marketingOptInboolean or null
The person's marketing consent, kept equal to the opposite of the account's marketing_opt_out: set from the sign-up form's box, then by update-user, PATCH /v2/account/profile and the unsubscribe link alike.
user.twoFactorEnabledboolean or null
Whether a second factor is live for this user (set when an enrolment is confirmed, cleared by /two-factor/disable).

400 error

Bad request. From Taigan's Turnstile guard (problem+json): captcha_missing (T108: the problem's own code; errors: [{code: "captcha_missing"}] repeats it) (no x-captcha-response header). From better-auth (JSON {code, message}): VALIDATION_ERROR (a required field missing or the wrong type), INVALID_EMAIL, PASSWORD_TOO_SHORT (under 10), PASSWORD_TOO_LONG (over 128), BAD_REQUEST (malformed JSON).

As application/problem+json, application/json.

403 error

Forbidden. From Taigan's Turnstile guard (problem+json): captcha_failed (Cloudflare rejected the token). From better-auth (JSON): INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (the request carries an Origin, Referer or cookie and the origin is not one of the app's trusted origins), CROSS_SITE_NAVIGATION_LOGIN_BLOCKED (a cross-site form navigation), INVALID_CALLBACK_URL (callbackURL not on a trusted origin).

As application/problem+json, application/json.

415 error

Unsupported media type (better-auth JSON). Code: UNSUPPORTED_MEDIA_TYPE (the body is neither JSON nor form-encoded).

As application/json.

422 error

Unprocessable (better-auth JSON). Code: FAILED_TO_CREATE_USER (the user could not be written).

As application/json.

429 error

better-auth's request limiter (production): 3 sign-ups per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.
X-Retry-After header
Seconds to wait (better-auth's own header; Retry-After carries the same value).

503 error

Service unavailable (problem+json). Code: unavailable (Taigan could not reach Cloudflare's Turnstile verification service).

As application/problem+json.

Problem codes: captcha_missing (400), captcha_failed (403), unavailable (503). Every problem is a problem document.

POST /v2/auth/sign-in/email

Sign in with email and password (sets the session cookie, or starts a second-factor challenge)

Credentials: none. Operation id authSignInEmail.

Anonymous. On success sets the session cookie (dtg.session_token, __Secure- prefixed on https; HttpOnly, SameSite=Lax) and answers the user. Creating the session deletes every other session of the person (one active session per person, newest wins; an auth.session_evicted audit row is written when one was ended), records the device (an auth.signin audit row; a 'new sign-in from a new device' mail and bell notice when a device appears after others are known) and stamps presence (which cancels an open ownership claim on an organization the person owns). If the person has a live second factor, no session is created: the answer is {twoFactorRedirect: true, twoFactorMethods} and a challenge cookie (dtg.two_factor, 10 minutes) is set; finish with POST /v2/auth/two-factor/verify-totp or /two-factor/verify-backup-code (a device trusted within 30 days skips the challenge). Every refused attempt writes an auth.signin_failed audit row naming a hash of the address. Taigan's per-address throttle counts every attempt and is cleared by a success. Accepts application/json or application/x-www-form-urlencoded. API integrations do not sign in: they use an API key (Authorization: Bearer).

Request body

Required; application/json.

emailstring (email)required
The sign-in address (case-insensitive).
passwordstringrequired
The password.
rememberMeboolean
False makes the session cookie a browser-session cookie (no Max-Age) and sets dtg.dont_remember; the server-side session still slides as usual. (default true)
callbackURLstring
If given, the answer carries redirect: true, url and a Location header with this URL (still status 200). Must be relative or on a trusted origin.

Responses

200 success

Signed in, or a second-factor challenge started.

As application/json.

Location header
The callbackURL, when one was sent and the sign-in completed.
redirectboolean
True when callbackURL was sent (the client should navigate to url).
tokenstring
The new session's token (also in the session cookie).
urlstring or null
The callbackURL, or absent/null.
userAuthUser
The better-auth user (the identity behind a Taigan account; one to one with accounts). Timestamps are ISO 8601 strings.
user.idstring (uuid)always present
The auth user's id (auth_users.id). Not the Taigan account id that /v2/account reports.
user.namestringalways present
The name given at sign-up or set through POST /v2/auth/update-user. Mirrored into the account's display_name.
user.emailstring (email)always present
The sign-in address, lower-cased.
user.emailVerifiedbooleanalways present
Whether the address has been proved by its verification link. Sign-in is refused until it is true.
user.imagestring or null
A profile image URL. Stored if supplied; Taigan and Kelpie never show it.
user.createdAtstring (date-time)always present
When the user was created.
user.updatedAtstring (date-time)always present
When the user was last changed.
user.orgNamestring or null
The organization name typed on the sign-up form, used once to name the person's own organization. Never read again; the organization's real name is on /v2/account.
user.marketingOptInboolean or null
The person's marketing consent, kept equal to the opposite of the account's marketing_opt_out: set from the sign-up form's box, then by update-user, PATCH /v2/account/profile and the unsubscribe link alike.
user.twoFactorEnabledboolean or null
Whether a second factor is live for this user (set when an enrolment is confirmed, cleared by /two-factor/disable).
twoFactorRedirecttrue
Always true: the password was right and a second factor is required. No session cookie was set; the dtg.two_factor challenge cookie was.
twoFactorMethodsarray of "totp" | "otp"
The factors that can answer the challenge. In this deployment ["totp"] (a backup code is always accepted too); otp never appears because mailed codes are not configured.

400 error

Bad request (better-auth JSON). Codes: VALIDATION_ERROR (email or password missing or not a string), INVALID_EMAIL (not an address), BAD_REQUEST (malformed JSON).

As application/json.

401 error

Unauthorized (better-auth JSON). Codes: INVALID_EMAIL_OR_PASSWORD (no such user, no password set, or the wrong password: one answer for all three), FAILED_TO_CREATE_SESSION.

As application/json.

403 error

Forbidden (better-auth JSON). Codes: EMAIL_NOT_VERIFIED (the password was right but the address is not verified yet; no mail is re-sent, use /v2/auth/send-verification-email), INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (origin not trusted), CROSS_SITE_NAVIGATION_LOGIN_BLOCKED, INVALID_CALLBACK_URL.

As application/json.

415 error

Unsupported media type (better-auth JSON). Code: UNSUPPORTED_MEDIA_TYPE.

As application/json.

429 error

Too many requests. Two different refusals, both with Retry-After: Taigan's per-address throttle, code signin_throttled (more than 10 attempts for one address in an hour; body AuthSigninThrottled), or better-auth's per-IP limiter in production (3 sign-ins per 10 seconds; body {message} with no code, plus X-Retry-After).

As application/json.

Retry-After header
Seconds to wait before retrying.

Problem codes: signin_throttled (429). Every problem is a problem document.

POST /v2/auth/sign-in/social

Start (or complete with an ID token) a social sign-in. Not configured in this deployment

Credentials: none. Operation id authSignInSocial.

Anonymous. better-auth's OAuth sign-in. No social provider is configured in this deployment (Google is dormant until GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET are set, and O27 rules it off at launch), so every request that names a valid provider answers 404 PROVIDER_NOT_FOUND. Were a provider configured, the redirect form answers {url, redirect} with a Location header for the browser to follow to the provider (status 200), and the idToken form verifies the provider's ID token and signs in directly. A browser flow; an API client never needs it.

Request body

Required; application/json.

providerstringrequired
The provider id (one of better-auth's known providers, for example google). An unknown name is a 400 VALIDATION_ERROR; a known but unconfigured one is a 404.
callbackURLstring
Where to send the browser after a successful sign-in (relative or trusted origin).
newUserCallbackURLstring
Where to send a newly created user instead of callbackURL.
errorCallbackURLstring
Where to send the browser if the flow fails.
disableRedirectboolean
True to get the provider URL back without a Location header.
idTokenobject
An ID token already obtained from the provider, to sign in without a redirect (providers that support it).
idToken.tokenstringrequired
The provider's ID token.
idToken.noncestring
The nonce used when the token was issued.
idToken.accessTokenstring
The provider access token, if any.
idToken.refreshTokenstring
The provider refresh token, if any.
idToken.expiresAtnumber
Token expiry (epoch).
idToken.userobject
Profile data some providers send only once (Apple).
idToken.user.nameobject
The person's name parts.
idToken.user.name.firstNamestring
Given name.
idToken.user.name.lastNamestring
Family name.
idToken.user.emailstring
The address the provider reported.
scopesarray of string
Scopes to request instead of the provider defaults.
requestSignUpboolean
Explicitly allow creating a user when the provider disables implicit sign-up.
loginHintstring
A login hint forwarded to the provider.
additionalParamsobject
Extra query parameters for the provider's authorization URL (reserved OAuth keys are refused).
additionalDataobject
Opaque data carried through the OAuth state.

Responses

200 success

Redirect form: {url, redirect}. ID-token form: {redirect: false, token, user} and the session cookie. Not reachable in this deployment.

As application/json.

redirectbooleanalways present
True when a Location header to the provider was set.
urlstring
The provider's authorization URL (redirect form).
tokenstring
The new session token (ID-token form).
userAuthUser
The better-auth user (the identity behind a Taigan account; one to one with accounts). Timestamps are ISO 8601 strings.
user.idstring (uuid)always present
The auth user's id (auth_users.id). Not the Taigan account id that /v2/account reports.
user.namestringalways present
The name given at sign-up or set through POST /v2/auth/update-user. Mirrored into the account's display_name.
user.emailstring (email)always present
The sign-in address, lower-cased.
user.emailVerifiedbooleanalways present
Whether the address has been proved by its verification link. Sign-in is refused until it is true.
user.imagestring or null
A profile image URL. Stored if supplied; Taigan and Kelpie never show it.
user.createdAtstring (date-time)always present
When the user was created.
user.updatedAtstring (date-time)always present
When the user was last changed.
user.orgNamestring or null
The organization name typed on the sign-up form, used once to name the person's own organization. Never read again; the organization's real name is on /v2/account.
user.marketingOptInboolean or null
The person's marketing consent, kept equal to the opposite of the account's marketing_opt_out: set from the sign-up form's box, then by update-user, PATCH /v2/account/profile and the unsubscribe link alike.
user.twoFactorEnabledboolean or null
Whether a second factor is live for this user (set when an enrolment is confirmed, cleared by /two-factor/disable).

400 error

Bad request (better-auth JSON). Code: VALIDATION_ERROR (missing or unknown provider, bad field types).

As application/json.

401 error

Unauthorized (better-auth JSON). Codes (ID-token form, only with a provider configured): INVALID_TOKEN, FAILED_TO_GET_USER_INFO, USER_EMAIL_NOT_FOUND, OAUTH_LINK_ERROR.

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (a cookie-carrying request from an untrusted origin), INVALID_CALLBACK_URL, INVALID_ERROR_CALLBACK_URL, INVALID_NEW_USER_CALLBACK_URL, EMAIL_NOT_VERIFIED (ID-token form).

As application/json.

404 error

Not found (better-auth JSON). Codes: PROVIDER_NOT_FOUND (always, in this deployment), ID_TOKEN_NOT_SUPPORTED.

As application/json.

429 error

better-auth's request limiter (production): 3 per 10 seconds per IP on /sign-in/*. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/sign-out

Sign out: end the current session and clear the session cookie

Credentials: App session. Operation id authSignOut.

Deletes the session named by the cookie (if any) and expires the session cookies. Succeeds without a session too (nothing to end). Other /v2 routes cache a session lookup for up to 15 seconds, so the ended cookie can still be honored there for that long. The optional body only matters for a social provider with an end-session URL, which this deployment does not have.

Request body

Optional; application/json.

callbackURLstring
Post-logout redirect for a provider logout (unused here).
disableRedirectboolean
Return a provider logout URL without a Location header (unused here).
statestring
State passed to a provider logout endpoint (unused here).

Responses

200 success

Signed out.

As application/json.

successbooleanalways present
Always true.
urlstring
A provider logout URL. Never present in this deployment.
redirectboolean
Whether to follow url. Never present in this deployment.

400 error

Bad request (better-auth JSON). Code: VALIDATION_ERROR (body fields of the wrong type).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (a cookie-carrying POST must come from a trusted origin), INVALID_CALLBACK_URL.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

GET /v2/auth/get-session

The current session and user, or null

Credentials: App session. Operation id authGetSession.

Reads the session cookie. Answers null (status 200) when there is no cookie, or the session has expired or been revoked (an expired session's row and cookie are then deleted). Extends a session used more than a day after its last extension (a fresh expiresAt 7 days out and a re-set cookie). Answers Cache-Control: no-store. Note this is better-auth's view: the rest of /v2 additionally refuses a session 30 days after sign-in, an unverified user or a closed account, so a non-null answer here does not guarantee /v2/account will accept the cookie. Kelpie uses it to restore a signed-in tab.

Parameters

disableRefreshquery · boolean
True to read the session without extending it.
disableCookieCachequery · boolean
True to bypass better-auth's cookie cache. The cache is not enabled here, so it has no effect.

Responses

200 success

The session and user, or null when signed out.

As application/json.

sessionAuthSession
A better-auth session row. Taigan keeps one active session per person: creating a session deletes every other session of that person (ORG_LICENSING 4.2), so lists hold at most one entry.
session.idstring (uuid)always present
The session's id (the same id GET /v2/account/sessions reports).
session.tokenstringalways present
The session's bearer secret: the unsigned value inside the session cookie. Treat it as a credential. POST /v2/auth/revoke-session takes it.
session.userIdstring (uuid)always present
The auth user's id.
session.expiresAtstring (date-time)always present
When the session expires if not used. Sliding: a use more than a day after the last extension moves it to 7 days (SESSION_SLIDING_DAYS) from then. Taigan also ends every session 30 days (SESSION_MAX_DAYS) after createdAt whatever this says.
session.createdAtstring (date-time)always present
When the session was created (the sign-in).
session.updatedAtstring (date-time)always present
When the session was last extended.
session.ipAddressstring or null
The client IP that created the session, as Taigan's proxy configuration resolved it.
session.userAgentstring or null
The User-Agent that created the session.
userAuthUser
The better-auth user (the identity behind a Taigan account; one to one with accounts). Timestamps are ISO 8601 strings.
user.idstring (uuid)always present
The auth user's id (auth_users.id). Not the Taigan account id that /v2/account reports.
user.namestringalways present
The name given at sign-up or set through POST /v2/auth/update-user. Mirrored into the account's display_name.
user.emailstring (email)always present
The sign-in address, lower-cased.
user.emailVerifiedbooleanalways present
Whether the address has been proved by its verification link. Sign-in is refused until it is true.
user.imagestring or null
A profile image URL. Stored if supplied; Taigan and Kelpie never show it.
user.createdAtstring (date-time)always present
When the user was created.
user.updatedAtstring (date-time)always present
When the user was last changed.
user.orgNamestring or null
The organization name typed on the sign-up form, used once to name the person's own organization. Never read again; the organization's real name is on /v2/account.
user.marketingOptInboolean or null
The person's marketing consent, kept equal to the opposite of the account's marketing_opt_out: set from the sign-up form's box, then by update-user, PATCH /v2/account/profile and the unsubscribe link alike.
user.twoFactorEnabledboolean or null
Whether a second factor is live for this user (set when an enrolment is confirmed, cleared by /two-factor/disable).

400 error

Bad request (better-auth JSON). Code: VALIDATION_ERROR (a query flag that is not a boolean).

As application/json.

401 error

Unauthorized (better-auth JSON). Code: FAILED_TO_GET_SESSION (the session vanished while being extended; the cookie is cleared).

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

500 error

Server error (better-auth JSON). Code: FAILED_TO_GET_SESSION.

As application/json.

POST /v2/auth/get-session

POST form of get-session. Always refused in this deployment

Credentials: App session. Operation id authGetSessionPost.

better-auth serves POST /get-session only when session.deferSessionRefresh is on, which Taigan does not set, so a well-formed request always answers 405 METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED. Use GET /v2/auth/get-session.

Parameters

disableRefreshquery · boolean
As on the GET form (never reached).
disableCookieCachequery · boolean
As on the GET form (never reached).

Responses

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (a cookie-carrying POST from an untrusted origin is refused before the method check).

As application/json.

405 error

Method not allowed (better-auth JSON). Code: METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED (always).

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

GET /v2/auth/verify-email

The mailed verification link: verifies the address (or a change of address) and signs the person in

Credentials: App session. Operation id authVerifyEmail.

Anonymous; a browser follows it from the verification mail (through the app's /v2 proxy). The token is a signed link valid for EMAIL_VERIFICATION_LINK_SECONDS (1 hour). Three cases. (1) Sign-up verification: marks the address verified, stamps the account verified, and signs the person in (sets the session cookie; the session-create effects of sign-in apply). If a contested sign-up discarded the password (AUTH-2), a password-reset link is mailed so the person can choose one. An already-verified address changes nothing and creates no session. (2) Change of address (from POST /v2/auth/change-email): moves the sign-in address to the new one, verified, and sets a session. Membership is never touched. (3) Errors: with callbackURL the browser is redirected to callbackURL?error=<CODE> (with &lifetime_seconds= for TOKEN_EXPIRED and INVALID_TOKEN); without it the error is a JSON 401. A browser uses this; an API client never needs it.

Parameters

tokenquery · stringrequired
The verification token from the mailed link.
callbackURLquery · string
Where to send the browser afterwards (relative or on a trusted origin). Without it the answer is JSON.

Responses

200 success

Verified (no callbackURL). user is null for a sign-up verification or an already-verified address, and the updated user for a change of address.

As application/json.

statusbooleanalways present
Always true.
userAuthUser or nullalways present
The user after a change of address; null otherwise.
user.idstring (uuid)
The auth user's id (auth_users.id). Not the Taigan account id that /v2/account reports.
user.namestring
The name given at sign-up or set through POST /v2/auth/update-user. Mirrored into the account's display_name.
user.emailstring (email)
The sign-in address, lower-cased.
user.emailVerifiedboolean
Whether the address has been proved by its verification link. Sign-in is refused until it is true.
user.imagestring or null
A profile image URL. Stored if supplied; Taigan and Kelpie never show it.
user.createdAtstring (date-time)
When the user was created.
user.updatedAtstring (date-time)
When the user was last changed.
user.orgNamestring or null
The organization name typed on the sign-up form, used once to name the person's own organization. Never read again; the organization's real name is on /v2/account.
user.marketingOptInboolean or null
The person's marketing consent, kept equal to the opposite of the account's marketing_opt_out: set from the sign-up form's box, then by update-user, PATCH /v2/account/profile and the unsubscribe link alike.
user.twoFactorEnabledboolean or null
Whether a second factor is live for this user (set when an enrolment is confirmed, cleared by /two-factor/disable).

302 redirect

Redirect to callbackURL on success, or to callbackURL?error=<CODE> on failure (TOKEN_EXPIRED or INVALID_TOKEN with &lifetime_seconds=<seconds>, USER_NOT_FOUND, INVALID_USER).

Location header
The destination.

400 error

Bad request (better-auth JSON). Code: VALIDATION_ERROR (no token).

As application/json.

401 error

Unauthorized (no callbackURL; better-auth JSON). Codes: TOKEN_EXPIRED and INVALID_TOKEN (both with lifetime_seconds, body AuthLinkExpired), USER_NOT_FOUND (the address no longer has a user), INVALID_USER (a change-of-address link opened while signed in as someone else).

As application/json.

403 error

Forbidden (better-auth JSON). Code: INVALID_CALLBACK_URL (callbackURL is not relative and not on a trusted origin).

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

500 error

Server error (better-auth JSON). Code: FAILED_TO_CREATE_SESSION.

As application/json.

POST /v2/auth/send-verification-email

Send a new verification link

Credentials: App session. Operation id authSendVerificationEmail.

Anonymous (the 'Send a new link' button after sign-up or after EMAIL_NOT_VERIFIED). Without a session: always answers {status: true} after at least half a second, whether or not the address exists; a mail is sent only when the address has an unverified user. With a session: the address must be the session's own and still unverified, which a Taigan session never is (sign-in requires verification), so a signed-in call answers EMAIL_ALREADY_VERIFIED. The new link is valid for EMAIL_VERIFICATION_LINK_SECONDS (1 hour).

Request body

Required; application/json.

emailstring (email)required
The address to send the link to.
callbackURLstring
Where the link sends the browser after verifying (relative or trusted origin). Defaults to /.

Responses

200 success

Accepted (the same answer whether or not a mail was sent).

As application/json.

statusbooleanalways present
Always true on success.

400 error

Bad request (better-auth JSON). Codes: VALIDATION_ERROR (missing or malformed email), EMAIL_MISMATCH (signed in, and email is not the session's address), EMAIL_ALREADY_VERIFIED (signed in).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (cookie-carrying POST from an untrusted origin), INVALID_CALLBACK_URL.

As application/json.

429 error

better-auth's request limiter (production): 3 per 60 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/request-password-reset

Mail a password-reset link (Turnstile-guarded)

Credentials: none. Operation id authRequestPasswordReset.

Anonymous. Requires a Turnstile token in the x-captcha-response header (checked by Taigan first). Answers the same 200 whether or not the address has a user (anti-enumeration); when it does, a reset link valid for PASSWORD_RESET_LINK_SECONDS (1 hour) is mailed. The link is GET /v2/auth/reset-password/{token}?callbackURL=<redirectTo>, which sends the browser to redirectTo?token=...; the page then calls POST /v2/auth/reset-password.

Parameters

x-captcha-responseheader · stringrequired
The Cloudflare Turnstile widget token for this form submission.

Request body

Required; application/json.

emailstring (email)required
The account's address.
redirectTostring
The app page that sets the new password (Kelpie: <app>/reset). Relative or on a trusted origin. The mailed link sends the browser there with ?token= (or ?error=INVALID_TOKEN&lifetime_seconds= when the link is spent or expired).

Responses

200 success

Accepted.

As application/json.

statusbooleanalways present
Always true.
messagestringalways present
If this email exists in our system, check your email for the reset link.

400 error

Bad request. From Taigan's Turnstile guard (problem+json): captcha_missing (T108: the problem's own code; errors: [{code: "captcha_missing"}] repeats it). From better-auth (JSON): VALIDATION_ERROR (missing or malformed email).

As application/problem+json, application/json.

403 error

Forbidden. From Taigan's Turnstile guard (problem+json): captcha_failed. From better-auth (JSON): INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (cookie-carrying POST from an untrusted origin), INVALID_REDIRECT_URL / INVALID_CALLBACK_URL (redirectTo not relative and not on a trusted origin).

As application/problem+json, application/json.

429 error

better-auth's request limiter (production): 3 per 60 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

503 error

Service unavailable (problem+json). Code: unavailable (Cloudflare's Turnstile verification could not be reached).

As application/problem+json.

Problem codes: captcha_missing (400), captcha_failed (403), unavailable (503). Every problem is a problem document.

POST /v2/auth/reset-password

Set a new password with a reset token

Credentials: none. Operation id authResetPassword.

Anonymous (the reset page posts the token it was redirected with). Consumes the token (one use), sets the password (creating the email-and-password credential if the account had none, as after an AUTH-2 discard), mails a 'your password was changed' confirmation, writes a bell notice saying other sessions were signed out, and deletes every session of the person. It revokes no API key: the mail and the notice list the person's active keys (name, prefix and last four, organization, created, last used) and say to revoke any that is not theirs. It does not sign the person in: they sign in with the new password. Not Turnstile-guarded.

Parameters

tokenquery · string
The reset token, if not sent in the body (the body wins).

Request body

Required; application/json.

newPasswordstringrequired
The new password: 10 to 128 characters. (at most 128 characters)
tokenstring
The reset token from the redirect's ?token=.

Responses

200 success

Password set; every session ended.

As application/json.

statusbooleanalways present
Always true on success.

400 error

Bad request (better-auth JSON). Codes: INVALID_TOKEN (no token, or unknown, spent or expired: indistinguishable; carries lifetime_seconds, body AuthLinkExpired), PASSWORD_TOO_SHORT, PASSWORD_TOO_LONG, USER_NOT_FOUND (the user was deleted after the mail), VALIDATION_ERROR (no newPassword).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (cookie-carrying POST from an untrusted origin).

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/change-password

Change the password (current password required)

Credentials: App session. Operation id authChangePassword.

Session only. Checks currentPassword, then sets newPassword. With revokeOtherSessions: true every session is deleted and a new one is created and set in the cookie (its token is returned). No mail or notice is sent for a change (only a reset sends them). Kelpie does not call this today.

Request body

Required; application/json.

currentPasswordstringrequired
The current password.
newPasswordstringrequired
The new password: 10 to 128 characters. (at most 128 characters)
revokeOtherSessionsboolean
True to end every session and start a fresh one for this browser.

Responses

200 success

Password changed.

As application/json.

tokenstring or nullalways present
The new session's token when revokeOtherSessions was true; otherwise null.
userAuthUseralways present
The better-auth user (the identity behind a Taigan account; one to one with accounts). Timestamps are ISO 8601 strings.
user.idstring (uuid)always present
The auth user's id (auth_users.id). Not the Taigan account id that /v2/account reports.
user.namestringalways present
The name given at sign-up or set through POST /v2/auth/update-user. Mirrored into the account's display_name.
user.emailstring (email)always present
The sign-in address, lower-cased.
user.emailVerifiedbooleanalways present
Whether the address has been proved by its verification link. Sign-in is refused until it is true.
user.imagestring or null
A profile image URL. Stored if supplied; Taigan and Kelpie never show it.
user.createdAtstring (date-time)always present
When the user was created.
user.updatedAtstring (date-time)always present
When the user was last changed.
user.orgNamestring or null
The organization name typed on the sign-up form, used once to name the person's own organization. Never read again; the organization's real name is on /v2/account.
user.marketingOptInboolean or null
The person's marketing consent, kept equal to the opposite of the account's marketing_opt_out: set from the sign-up form's box, then by update-user, PATCH /v2/account/profile and the unsubscribe link alike.
user.twoFactorEnabledboolean or null
Whether a second factor is live for this user (set when an enrolment is confirmed, cleared by /two-factor/disable).

400 error

Bad request (better-auth JSON). Codes: INVALID_PASSWORD (currentPassword is wrong), PASSWORD_TOO_SHORT, PASSWORD_TOO_LONG, CREDENTIAL_ACCOUNT_NOT_FOUND (the account has no password yet; use the reset flow), VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (the POST must come from a trusted origin).

As application/json.

429 error

better-auth's request limiter (production): 3 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

500 error

Server error (better-auth JSON). Code: FAILED_TO_GET_SESSION (the replacement session could not be created).

As application/json.

POST /v2/auth/change-email

Request a change of sign-in address (verified at the new address)

Credentials: App session. Operation id authChangeEmail.

Session only. Nothing changes yet: a verification link is mailed to the NEW address, and a notice naming the new address is mailed to the OLD one at once. The address moves when the link (GET /v2/auth/verify-email) is followed, valid for EMAIL_VERIFICATION_LINK_SECONDS; accounts.email follows it. Membership is never affected. If the new address already belongs to another user, the answer is the same {status: true} and nothing is sent (anti-enumeration).

Request body

Required; application/json.

newEmailstring (email)required
The address to move to (stored lower-cased).
callbackURLstring
Where the verification link sends the browser afterwards (relative or trusted origin). Defaults to /.

Responses

200 success

Accepted.

As application/json.

statusbooleanalways present
Always true on success.

400 error

Bad request (better-auth JSON). Codes: VALIDATION_ERROR (missing or malformed newEmail); no code, message Email is the same (the new address is the current one).

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN, INVALID_CALLBACK_URL.

As application/json.

429 error

better-auth's request limiter (production): 3 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/update-user

Update the auth user's name (and other user fields)

Credentials: App session. Operation id authUpdateUser.

Session only. Sets any of name, image, orgName, marketingOptIn on the auth user; name is mirrored into the account's display_name. marketingOptIn is the person's consent to release notes and product news: it is stored as the account's marketing_opt_out (its opposite), which is what every mail reads and what GET /v2/account/profile shows, in both directions, and a change writes an account.marketing_changed audit row. orgName here only changes the auth user's copy, which nothing reads after sign-up, so it renames no organization (use PATCH /v2/account/organization). The address cannot be changed here (use /v2/auth/change-email).

Request body

Required; application/json.

namestring
The person's name (mirrored to display_name).
imagestring or null
A profile image URL (stored, never shown).
orgNamestring
Overwrites the sign-up form's copy only; renames nothing.
marketingOptInboolean
Consent to release notes and product news; stored as the account's marketing_opt_out (true here is marketing_opt_out: false), audited as account.marketing_changed when it changes.

Responses

200 success

Updated (the session cookie is re-set).

As application/json.

statusbooleanalways present
Always true on success.

400 error

Bad request (better-auth JSON). Codes: EMAIL_CAN_NOT_BE_UPDATED (email sent), FIELD_NOT_ALLOWED (twoFactorEnabled sent), BODY_MUST_BE_AN_OBJECT, VALIDATION_ERROR; no code, message No fields to update (none of the four fields sent).

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/update-session

Update custom session fields. None exist in this deployment

Credentials: App session. Operation id authUpdateSession.

Session only. better-auth lets a deployment declare extra session fields and set them here; Taigan declares none, so every body (with a live session) answers 400 with the message No fields to update.

Request body

Required; application/json.

Responses

200 success

Never answered in this deployment. Would be {session}.

As application/json.

sessionAuthSessionalways present
A better-auth session row. Taigan keeps one active session per person: creating a session deletes every other session of that person (ORG_LICENSING 4.2), so lists hold at most one entry.
session.idstring (uuid)always present
The session's id (the same id GET /v2/account/sessions reports).
session.tokenstringalways present
The session's bearer secret: the unsigned value inside the session cookie. Treat it as a credential. POST /v2/auth/revoke-session takes it.
session.userIdstring (uuid)always present
The auth user's id.
session.expiresAtstring (date-time)always present
When the session expires if not used. Sliding: a use more than a day after the last extension moves it to 7 days (SESSION_SLIDING_DAYS) from then. Taigan also ends every session 30 days (SESSION_MAX_DAYS) after createdAt whatever this says.
session.createdAtstring (date-time)always present
When the session was created (the sign-in).
session.updatedAtstring (date-time)always present
When the session was last extended.
session.ipAddressstring or null
The client IP that created the session, as Taigan's proxy configuration resolved it.
session.userAgentstring or null
The User-Agent that created the session.

400 error

Bad request (better-auth JSON). Codes: BODY_MUST_BE_AN_OBJECT, VALIDATION_ERROR; no code, message No fields to update (always, for an object body).

As application/json.

401 error

Unauthorized (better-auth JSON). Codes: UNAUTHORIZED (no live session), FAILED_TO_GET_SESSION.

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/verify-password

Check the signed-in person's password

Credentials: App session. Operation id authVerifyPassword.

Session only. Answers {status: true} if password is the person's current password; changes nothing. better-auth marks it server-scoped for its own client, but the router serves it over HTTP.

Request body

Required; application/json.

passwordstringrequired
The password to check.

Responses

200 success

The password is correct.

As application/json.

statusbooleanalways present
Always true on success.

400 error

Bad request (better-auth JSON). Codes: INVALID_PASSWORD (wrong, or the account has no password), VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/delete-user

Delete the user. Disabled in this deployment (use POST /v2/account/close)

Credentials: App session. Operation id authDeleteUser.

Session only. better-auth's self-deletion is not enabled in Taigan (user.deleteUser is not configured), so a signed-in request answers 404 with an empty body. Closing an account is POST /v2/account/close, which handles memberships, keys, billing and the audit trail.

Request body

Optional; application/json.

passwordstring
The current password (would be checked if the feature were on).
tokenstring
A deletion-confirmation token (unused here).
callbackURLstring
Where a confirmation link would send the browser (unused here).

Responses

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN, INVALID_CALLBACK_URL.

As application/json.

404 error

Not found: the feature is disabled. Always, for a signed-in caller. Empty body.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

GET /v2/auth/delete-user/callback

Deletion-confirmation link. Disabled in this deployment

Credentials: none. Operation id authDeleteUserCallback.

The link better-auth would mail to confirm a self-deletion. Disabled here like /delete-user: a request with a token answers 404 NOT_FOUND. A browser link; an API client never needs it.

Parameters

tokenquery · stringrequired
The deletion-confirmation token.
callbackURLquery · string
Where to send the browser afterwards (relative or trusted origin).

Responses

400 error

Bad request (better-auth JSON). Code: VALIDATION_ERROR (no token).

As application/json.

403 error

Forbidden (better-auth JSON). Code: INVALID_CALLBACK_URL.

As application/json.

404 error

Not found (better-auth JSON). Code: NOT_FOUND (always: the feature is disabled).

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

GET /v2/auth/list-sessions

List the person's active sessions (better-auth's view)

Credentials: App session. Operation id authListSessions.

Session only, and the session must be fresh (created within the last 24 hours), else 403 SESSION_NOT_FRESH. Lists unexpired sessions, each with its token. Under Taigan's one-session policy the list holds the current session only. The Taigan surface for this (with current, IP and user agent, working for API keys too) is GET /v2/account/sessions.

Responses

200 success

The sessions.

As application/json.

[].idstring (uuid)always present
The session's id (the same id GET /v2/account/sessions reports).
[].tokenstringalways present
The session's bearer secret: the unsigned value inside the session cookie. Treat it as a credential. POST /v2/auth/revoke-session takes it.
[].userIdstring (uuid)always present
The auth user's id.
[].expiresAtstring (date-time)always present
When the session expires if not used. Sliding: a use more than a day after the last extension moves it to 7 days (SESSION_SLIDING_DAYS) from then. Taigan also ends every session 30 days (SESSION_MAX_DAYS) after createdAt whatever this says.
[].createdAtstring (date-time)always present
When the session was created (the sign-in).
[].updatedAtstring (date-time)always present
When the session was last extended.
[].ipAddressstring or null
The client IP that created the session, as Taigan's proxy configuration resolved it.
[].userAgentstring or null
The User-Agent that created the session.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Code: SESSION_NOT_FRESH (the session is more than 24 hours old; sign in again).

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

500 error

Server error (better-auth JSON, INTERNAL_SERVER_ERROR status text): the sessions could not be read.

POST /v2/auth/revoke-session

End one of the person's sessions by its token

Credentials: App session. Operation id authRevokeSession.

Session only. Deletes the session whose token is given, if it is the caller's; a token that is unknown or someone else's is ignored and the answer is the same {status: true}. Revoking the current session's token signs this browser out (the cookie stays until it is next refused). Other /v2 routes may honor an ended session for up to 15 seconds (their lookup cache). The Taigan equivalent by session id is DELETE /v2/account/sessions/{id}.

Request body

Required; application/json.

tokenstringrequired
The session token to end (from /v2/auth/list-sessions).

Responses

200 success

Done (or nothing to do).

As application/json.

statusbooleanalways present
Always true on success.

400 error

Bad request (better-auth JSON). Code: VALIDATION_ERROR (no token).

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

500 error

Server error (better-auth JSON). Code: INTERNAL_SERVER_ERROR (the delete failed).

As application/json.

POST /v2/auth/revoke-sessions

End every session of the person, this one included

Credentials: App session. Operation id authRevokeSessions.

Session only. Deletes all of the person's sessions, the current one included (the browser is signed out; the cookie is not cleared but is refused from then on, within 15 seconds on other /v2 routes). The Taigan equivalent is POST /v2/account/sessions/sign-out-everywhere.

Responses

200 success

Every session ended.

As application/json.

statusbooleanalways present
Always true on success.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

500 error

Server error (better-auth JSON). Code: INTERNAL_SERVER_ERROR.

As application/json.

POST /v2/auth/revoke-other-sessions

End every session of the person except this one

Credentials: App session. Operation id authRevokeOtherSessions.

Session only. Deletes the person's other unexpired sessions and keeps the current one. Under Taigan's one-session policy there are normally none to end (every sign-in already evicts the others), so this is usually a no-op that answers {status: true}.

Responses

200 success

Done.

As application/json.

statusbooleanalways present
Always true on success.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

GET /v2/auth/list-accounts

List the sign-in methods linked to the person

Credentials: App session. Operation id authListAccounts.

Session only. In this deployment the list holds one entry, providerId: "credential" (email and password), because no social provider is configured. An account whose password was discarded (AUTH-2) still has the entry.

Responses

200 success

The linked sign-in methods.

As application/json.

[].idstring (uuid)always present
The linked account's id (what accountId means in /unlink-account, /get-access-token, /refresh-token and /account-info).
[].providerIdstringalways present
credential for email and password (the only kind this deployment creates), or a social provider id such as google.
[].accountIdstringalways present
The provider's own id for the person (for credential, the auth user's id).
[].issuerstringalways present
The issuer the provider id is scoped to (a local issuer for credential).
[].userIdstring (uuid)always present
The auth user's id.
[].scopesarray of stringalways present
OAuth scopes granted by the provider; empty for credential.
[].createdAtstring (date-time)always present
When the method was linked.
[].updatedAtstring (date-time)always present
When it was last changed (for credential, the last password change).

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/link-social

Link a social sign-in to the person. Not configured in this deployment

Credentials: App session. Operation id authLinkSocial.

Session only. No social provider is configured, so a request naming a valid provider answers 404 PROVIDER_NOT_FOUND. Were one configured, the redirect form answers {url, redirect} (with a Location header) and the provider returns the browser to /v2/auth/callback/{id}; the ID-token form links directly and answers {url: "", status: true, redirect: false}.

Request body

Required; application/json.

providerstringrequired
The provider id (one of better-auth's known providers). Unknown names are a 400.
callbackURLstring
Where to send the browser after linking.
errorCallbackURLstring
Where to send the browser if linking fails.
disableRedirectboolean
Return the provider URL without a Location header.
idTokenobject
A provider ID token, to link without a redirect.
idToken.tokenstringrequired
The provider's ID token.
idToken.noncestring
The nonce used when it was issued.
idToken.accessTokenstring
The provider access token, if any.
idToken.refreshTokenstring
The provider refresh token, if any.
scopesarray of string
Additional scopes to request.
requestSignUpboolean
Allow sign-up where the provider disables implicit sign-up.
loginHintstring
A login hint forwarded to the provider.
additionalParamsobject
Extra query parameters for the provider's authorization URL.
additionalDataobject
Opaque data carried through the OAuth state.

Responses

200 success

Not reachable in this deployment.

As application/json.

urlstring
The provider authorization URL (redirect form), or empty (ID-token form).
redirectbooleanalways present
True when a Location header was set.
statusboolean
True when linked directly (ID-token form).

400 error

Bad request (better-auth JSON). Code: VALIDATION_ERROR (missing or unknown provider).

As application/json.

401 error

Unauthorized (better-auth JSON). Codes: UNAUTHORIZED (no live session); with a provider configured also INVALID_TOKEN, FAILED_TO_GET_USER_INFO, USER_EMAIL_NOT_FOUND, LINKING_NOT_ALLOWED, LINKING_DIFFERENT_EMAILS_NOT_ALLOWED.

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN, INVALID_CALLBACK_URL, INVALID_ERROR_CALLBACK_URL.

As application/json.

404 error

Not found (better-auth JSON). Codes: PROVIDER_NOT_FOUND (always, in this deployment), ID_TOKEN_NOT_SUPPORTED.

As application/json.

409 error

Conflict (better-auth JSON). Code: SOCIAL_ACCOUNT_ALREADY_LINKED (provider configured only).

As application/json.

417 error

Expectation failed (better-auth JSON). Code: LINKING_FAILED (provider configured only).

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/unlink-account

Remove a linked sign-in method

Credentials: App session. Operation id authUnlinkAccount.

Session only, and the session must be fresh (under 24 hours old). The last remaining method can never be removed, and in this deployment every person has exactly one (credential), so the answer is 400 FAILED_TO_UNLINK_LAST_ACCOUNT.

Request body

Required; application/json.

accountIdstringrequired
The linked account's id from /v2/auth/list-accounts.

Responses

200 success

Removed (not reachable while a person has one method).

As application/json.

statusbooleanalways present
Always true on success.

400 error

Bad request (better-auth JSON). Codes: FAILED_TO_UNLINK_LAST_ACCOUNT (always, here), ACCOUNT_NOT_FOUND, VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: SESSION_NOT_FRESH (session over 24 hours old), INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/get-access-token

A social provider's access token for the person. Not usable in this deployment

Credentials: App session. Operation id authGetAccessToken.

Session only. Returns (refreshing if near expiry) the OAuth access token stored for a linked social account. With no social provider configured the only linked account is credential, which answers 400 PROVIDER_NOT_SUPPORTED. The body selects the account by accountId or by useAccountCookie: true (the account cookie is not enabled here, so that form answers ACCOUNT_NOT_FOUND); userId is ignored for an HTTP caller (the session's user always wins). No other properties are allowed.

Request body

Required; application/json.

accountIdstring
The linked account's id. Required unless useAccountCookie is sent.
useAccountCookietrue
Select the account from better-auth's account cookie instead of accountId (not enabled here).
userIdstring
Ignored over HTTP.

Responses

200 success

Not reachable in this deployment.

As application/json.

accessTokenstring
The provider access token.
accessTokenExpiresAtstring (date-time) or null
When it expires, if known.
scopesarray of string
Granted scopes.
idTokenstring
The provider ID token, if stored.

400 error

Bad request (better-auth JSON). Codes: PROVIDER_NOT_SUPPORTED (always for credential), ACCOUNT_NOT_FOUND (unknown accountId, or the cookie form), FAILED_TO_GET_ACCESS_TOKEN, VALIDATION_ERROR (neither selector, or an extra property).

As application/json.

401 error

Unauthorized: no live session. Empty body.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/refresh-token

Refresh a social provider's tokens for the person. Not usable in this deployment

Credentials: App session. Operation id authRefreshToken.

Session only. Uses the stored OAuth refresh token of a linked social account to mint new provider tokens. Selection works as for /v2/auth/get-access-token. With no social provider configured, the credential account answers 400 PROVIDER_NOT_SUPPORTED. These are the provider's tokens, not Taigan session or API-key tokens: a Taigan session is extended by use, and API keys do not expire this way.

Request body

Required; application/json.

accountIdstring
The linked account's id. Required unless useAccountCookie is sent.
useAccountCookietrue
Select from the account cookie (not enabled here).
userIdstring
Ignored over HTTP.

Responses

200 success

Not reachable in this deployment.

As application/json.

accessTokenstring
The new provider access token.
refreshTokenstring
The provider refresh token (new, or the existing one).
accessTokenExpiresAtstring (date-time) or null
When the access token expires.
refreshTokenExpiresAtstring (date-time) or null
When the refresh token expires.
scopestring or null
Granted scopes, comma-separated.
idTokenstring or null
The provider ID token.
providerIdstring
The provider id.
accountIdstring
The linked account's id.

400 error

Bad request (better-auth JSON). Codes: PROVIDER_NOT_SUPPORTED (always for credential), ACCOUNT_NOT_FOUND, TOKEN_REFRESH_NOT_SUPPORTED, REFRESH_TOKEN_NOT_FOUND, FAILED_TO_REFRESH_ACCESS_TOKEN, VALIDATION_ERROR.

As application/json.

401 error

Unauthorized: no live session. Empty body.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

GET /v2/auth/account-info

The social provider's profile for a linked account. Not usable in this deployment

Credentials: App session. Operation id authAccountInfo.

Session only. Fetches the person's profile from the provider of a linked social account. With no social provider configured, the credential account answers 400 PROVIDER_NOT_CONFIGURED. Selection is by query: accountId, or useAccountCookie=true; no other query parameters are allowed. For the person's Taigan profile use GET /v2/account.

Parameters

accountIdquery · string
The linked account's id from /v2/auth/list-accounts. Required unless useAccountCookie is sent.
useAccountCookiequery · true
true to select from better-auth's account cookie (not enabled here).
userIdquery · string
Ignored over HTTP (the session's user wins).

Responses

200 success

Not reachable in this deployment.

As application/json.

userobjectalways present
The profile as the provider reports it.
user.idstring
The provider's id for the person.
user.namestring
Name.
user.emailstring or null
Address.
user.imagestring or null
Picture URL.
user.emailVerifiedbooleanalways present
Whether the provider verified the address.
dataobjectalways present
The provider's raw profile payload.
accountobjectalways present
The linked account the profile came from.
account.idstringalways present
The linked account's id.
account.providerIdstringalways present
The provider id.
account.issuerstringalways present
The provider issuer.
account.accountIdstringalways present
The provider's id for the person.

400 error

Bad request (better-auth JSON). Codes: PROVIDER_NOT_CONFIGURED (always for credential), ACCOUNT_NOT_FOUND, ACCESS_TOKEN_NOT_FOUND, FAILED_TO_GET_ACCESS_TOKEN, PROVIDER_NOT_SUPPORTED, VALIDATION_ERROR (neither selector, or an extra query parameter).

As application/json.

401 error

Unauthorized. No live session (empty body), or (better-auth JSON) FAILED_TO_GET_USER_INFO from the provider.

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

GET /v2/auth/callback/{id}

OAuth redirect target for a social provider (browser only)

Credentials: none. Operation id authOAuthCallback.

Anonymous; the provider sends the browser here after a social sign-in or link. Always answers a redirect: to the flow's callbackURL (or newUserCallbackURL) with a session cookie on success, or to the error target (errorCallbackURL, else /v2/auth/error) with ?error=<code>. No social provider is configured in this deployment, so every request ends at the error target (state_not_found without state, state_mismatch or please_restart_the_process with an unknown one). A browser uses this; an API client never needs it.

Parameters

idpath · stringrequired
The provider id (for example google).
codequery · string
The authorization code from the provider.
statequery · string
The OAuth state better-auth issued.
errorquery · string
An error the provider reported.
error_descriptionquery · string
The provider's error description.
device_idquery · string
A provider-specific device id (some providers).
userquery · string
A provider-specific user payload (Apple).
issquery · string
The issuer the provider reports (checked against the provider).

Responses

302 redirect

Redirect to the flow's destination, or to the error target with ?error=<code> (always the latter here). Error codes include invalid_callback_request, state_not_found, state_mismatch, no_code, provider_not_found, invalid_code, unable_to_get_user_info, email_not_found, account_already_linked_to_different_user, unable_to_link_account.

Location header
The destination.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/callback/{id}

OAuth form-post redirect target (browser only)

Credentials: none. Operation id authOAuthCallbackPost.

Anonymous; for providers that return with an HTML form POST (Apple). Merges the body and query fields and redirects to the GET form of the same URL, which does the work. Accepts application/x-www-form-urlencoded or JSON. Not configured in this deployment (see the GET form). A browser uses this; an API client never needs it.

Parameters

idpath · stringrequired
The provider id.

Request body

Optional; application/x-www-form-urlencoded.

Responses

302 redirect

Redirect to GET /v2/auth/callback/{id}?<the same fields>.

Location header
The GET form's URL.

400 error

Bad request (better-auth JSON). Code: VALIDATION_ERROR (a field that is not a string).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (a cookie-carrying POST from an untrusted origin).

As application/json.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

GET /v2/auth/error

better-auth's error page (browser only)

Credentials: none. Operation id authErrorPage.

Anonymous; where a failed OAuth flow lands when no error URL was given. In production it redirects to the API host's /?error=<code>[&error_description=...]; elsewhere it renders a small HTML page naming the code. A browser uses this; an API client never needs it.

Parameters

errorquery · string
The error code to show (letters, digits, _, -, '; anything else shows as UNKNOWN).
error_descriptionquery · string
A description to show (HTML-escaped).

Responses

200 success

The HTML error page (not production).

As text/html.

302 redirect

Production: redirect to /?error=<code>.

Location header
The destination.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

GET /v2/auth/ok

Liveness of the auth handler

Credentials: none. Operation id authOk.

Anonymous. Answers {ok: true} when better-auth is mounted and answering. Touches no database. The service's own health check is GET /healthz.

Responses

200 success

The auth handler is up.

As application/json.

okbooleanalways present
Always true.

429 error

better-auth's request limiter (production): 100 per 10 seconds per IP. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/two-factor/enable

Start authenticator-app enrolment: returns the TOTP URI and backup codes

Credentials: App session. Operation id authTwoFactorEnable.

Session only; the current password is required. Generates a new TOTP secret and 10 backup codes and returns them; it does NOT arm the factor: enrolment takes effect when a code from the app is confirmed with POST /v2/auth/two-factor/verify-totp on this session (that writes the auth.mfa_enabled audit row). Calling it again replaces the secret and codes; if a factor is already live it stays live, so an authenticator set up from the old secret stops working. Only method: "totp" works here: mailed codes (otp) are not configured. Support operators must have a live factor.

Request body

Required; application/json.

passwordstringrequired
The person's current password.
method"totp" | "otp"
totp (an authenticator app). otp answers 400 OTP_NOT_CONFIGURED here. (default "totp")
issuerstring
Issuer label in the authenticator app. Defaults to Data Tactical Group.

Responses

200 success

The secret's URI and the backup codes. Show both once; they are not retrievable later (two_factor.backup_codes_remaining on /v2/account gives only a count).

As application/json.

method"totp"always present
Always totp here.
totpURIstringalways present
An otpauth://totp/... URI (6 digits, 30-second period) to render as a QR code.
backupCodesarray of stringalways present
10 one-time backup codes, each xxxxx-xxxxx (letters and digits).

400 error

Bad request (better-auth JSON). Codes: INVALID_PASSWORD (missing or wrong), OTP_NOT_CONFIGURED (method: otp), VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 3 per 10 seconds per IP on /two-factor/*. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/two-factor/verify-totp

Verify an authenticator code: confirm enrolment, or answer a sign-in challenge

Credentials: App session. Operation id authTwoFactorVerifyTotp.

Two uses. (1) With a session: confirms enrolment. The first correct code arms the factor (twoFactorEnabled true), replaces the session with a new one (new cookie and token) and writes an auth.mfa_enabled audit row; a correct code on an already-armed factor keeps the session and marks it as having passed the challenge (T110: how an unchallenged DTG admin session reaches /v2/admin). (2) Without a session, with the dtg.two_factor challenge cookie that POST /v2/auth/sign-in/email set: completes the sign-in, creating the session (with all the sign-in effects) and, with trustDevice: true, a dtg.trust_device cookie that skips the challenge on this browser for 30 days. Challenge limits: 5 wrong codes on one challenge end it (the sixth attempt answers 400 TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE; sign in again), and 10 wrong codes in a row across challenges lock the factor for 15 minutes (429 ACCOUNT_TEMPORARILY_LOCKED, checked before the code, so a correct code is refused while it stands). The lockout and per-challenge cap apply only to sign-in challenges, not to enrolment with a session.

Request body

Required; application/json.

codestringrequired
The 6-digit code from the authenticator app.
trustDeviceboolean
Sign-in challenge only: trust this browser for 30 days.

Responses

200 success

Verified.

As application/json.

tokenstring or null
The session token now in the session cookie (the new session on a sign-in challenge or on a first enrolment, otherwise the current one). Null or absent only where the operation says so.
userAuthUseralways present
The better-auth user (the identity behind a Taigan account; one to one with accounts). Timestamps are ISO 8601 strings.
user.idstring (uuid)always present
The auth user's id (auth_users.id). Not the Taigan account id that /v2/account reports.
user.namestringalways present
The name given at sign-up or set through POST /v2/auth/update-user. Mirrored into the account's display_name.
user.emailstring (email)always present
The sign-in address, lower-cased.
user.emailVerifiedbooleanalways present
Whether the address has been proved by its verification link. Sign-in is refused until it is true.
user.imagestring or null
A profile image URL. Stored if supplied; Taigan and Kelpie never show it.
user.createdAtstring (date-time)always present
When the user was created.
user.updatedAtstring (date-time)always present
When the user was last changed.
user.orgNamestring or null
The organization name typed on the sign-up form, used once to name the person's own organization. Never read again; the organization's real name is on /v2/account.
user.marketingOptInboolean or null
The person's marketing consent, kept equal to the opposite of the account's marketing_opt_out: set from the sign-up form's box, then by update-user, PATCH /v2/account/profile and the unsubscribe link alike.
user.twoFactorEnabledboolean or null
Whether a second factor is live for this user (set when an enrolment is confirmed, cleared by /two-factor/disable).

400 error

Bad request (better-auth JSON). Codes: TOTP_NOT_ENABLED (no enrolment started, or a sign-in challenge for a factor never confirmed), TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE (the challenge is spent; sign in again; does not count toward the lockout), VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Codes: INVALID_CODE (wrong code; on a challenge it counts toward both limits), INVALID_TWO_FACTOR_COOKIE (no session and no live challenge cookie: it expires after 10 minutes).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN (cookie-carrying POST from an untrusted origin).

As application/json.

429 error

Too many requests, with Retry-After. Either the second-factor lock, code ACCOUNT_TEMPORARILY_LOCKED (body AuthAccountLocked), or better-auth's per-IP limiter in production (3 per 10 seconds on /two-factor/*; body {message}, no code, plus X-Retry-After).

As application/json.

Retry-After header
Seconds to wait before retrying.

500 error

Server error (better-auth JSON). Codes: FAILED_TO_CREATE_SESSION, FAILED_TO_INVALIDATE_TWO_FACTOR_CHALLENGE.

As application/json.

POST /v2/auth/two-factor/verify-backup-code

Answer a sign-in challenge (or check a code) with a one-time backup code

Credentials: App session. Operation id authTwoFactorVerifyBackupCode.

Without a session, with the dtg.two_factor challenge cookie: completes the sign-in like /two-factor/verify-totp (same per-challenge cap of 5 and the same 10-failure lock). With a session: checks the code. Either way a correct code is consumed and cannot be used again. With disableSession: true the code is consumed but no session is created or changed (on a challenge, token is then absent and the challenge stays open). A backup code never arms or disarms a factor, so no audit row is written.

Request body

Required; application/json.

codestringrequired
One backup code, xxxxx-xxxxx.
disableSessionboolean
True to consume the code without creating a session or setting a cookie.
trustDeviceboolean
Sign-in challenge only: trust this browser for 30 days.

Responses

200 success

Accepted; the code is used up.

As application/json.

tokenstring or null
The session token now in the session cookie (the new session on a sign-in challenge or on a first enrolment, otherwise the current one). Null or absent only where the operation says so.
userAuthUseralways present
The better-auth user (the identity behind a Taigan account; one to one with accounts). Timestamps are ISO 8601 strings.
user.idstring (uuid)always present
The auth user's id (auth_users.id). Not the Taigan account id that /v2/account reports.
user.namestringalways present
The name given at sign-up or set through POST /v2/auth/update-user. Mirrored into the account's display_name.
user.emailstring (email)always present
The sign-in address, lower-cased.
user.emailVerifiedbooleanalways present
Whether the address has been proved by its verification link. Sign-in is refused until it is true.
user.imagestring or null
A profile image URL. Stored if supplied; Taigan and Kelpie never show it.
user.createdAtstring (date-time)always present
When the user was created.
user.updatedAtstring (date-time)always present
When the user was last changed.
user.orgNamestring or null
The organization name typed on the sign-up form, used once to name the person's own organization. Never read again; the organization's real name is on /v2/account.
user.marketingOptInboolean or null
The person's marketing consent, kept equal to the opposite of the account's marketing_opt_out: set from the sign-up form's box, then by update-user, PATCH /v2/account/profile and the unsubscribe link alike.
user.twoFactorEnabledboolean or null
Whether a second factor is live for this user (set when an enrolment is confirmed, cleared by /two-factor/disable).

400 error

Bad request (better-auth JSON). Codes: BACKUP_CODES_NOT_ENABLED (no factor enrolled), TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE (challenge spent; sign in again), VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Codes: INVALID_BACKUP_CODE (wrong or already used), INVALID_TWO_FACTOR_COOKIE (no session and no live challenge).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

409 error

Conflict (better-auth JSON, no code): message Failed to verify backup code. Please try again. (two requests used codes at the same instant; retry).

As application/json.

429 error

Too many requests, with Retry-After. Either ACCOUNT_TEMPORARILY_LOCKED (body AuthAccountLocked; sign-in challenges only) or better-auth's per-IP limiter (3 per 10 seconds on /two-factor/*; body {message}, no code).

As application/json.

Retry-After header
Seconds to wait before retrying.

500 error

Server error (better-auth JSON). Codes: FAILED_TO_CREATE_SESSION, FAILED_TO_INVALIDATE_TWO_FACTOR_CHALLENGE.

As application/json.

POST /v2/auth/two-factor/send-otp

Mail a one-time second-factor code. Not configured in this deployment

Credentials: App session. Operation id authTwoFactorSendOtp.

Mailed (OTP) codes are not configured in Taigan (the plugin has no sendOTP), so every request answers 400 OTP_NOT_CONFIGURED before anything else is checked. The authenticator app (verify-totp) and backup codes are the factors.

Request body

Optional; application/json.

trustDeviceboolean
Unused here.

Responses

400 error

Bad request (better-auth JSON). Codes: OTP_NOT_CONFIGURED (always), VALIDATION_ERROR.

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 3 per 10 seconds on /two-factor/*. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/two-factor/verify-otp

Verify a mailed second-factor code. Not usable in this deployment

Credentials: App session. Operation id authTwoFactorVerifyOtp.

Mailed codes are not configured (see /two-factor/send-otp), so no code can ever be pending: with a session or a live challenge cookie the answer is 400 OTP_HAS_EXPIRED, except that on a sign-in challenge a standing second-factor lock is reported first (429). Without either, 401 INVALID_TWO_FACTOR_COOKIE.

Request body

Required; application/json.

codestringrequired
The mailed code.
trustDeviceboolean
Sign-in challenge only: trust this browser for 30 days.

Responses

200 success

Never answered in this deployment.

As application/json.

tokenstring or null
The session token now in the session cookie (the new session on a sign-in challenge or on a first enrolment, otherwise the current one). Null or absent only where the operation says so.
userAuthUseralways present
The better-auth user (the identity behind a Taigan account; one to one with accounts). Timestamps are ISO 8601 strings.
user.idstring (uuid)always present
The auth user's id (auth_users.id). Not the Taigan account id that /v2/account reports.
user.namestringalways present
The name given at sign-up or set through POST /v2/auth/update-user. Mirrored into the account's display_name.
user.emailstring (email)always present
The sign-in address, lower-cased.
user.emailVerifiedbooleanalways present
Whether the address has been proved by its verification link. Sign-in is refused until it is true.
user.imagestring or null
A profile image URL. Stored if supplied; Taigan and Kelpie never show it.
user.createdAtstring (date-time)always present
When the user was created.
user.updatedAtstring (date-time)always present
When the user was last changed.
user.orgNamestring or null
The organization name typed on the sign-up form, used once to name the person's own organization. Never read again; the organization's real name is on /v2/account.
user.marketingOptInboolean or null
The person's marketing consent, kept equal to the opposite of the account's marketing_opt_out: set from the sign-up form's box, then by update-user, PATCH /v2/account/profile and the unsubscribe link alike.
user.twoFactorEnabledboolean or null
Whether a second factor is live for this user (set when an enrolment is confirmed, cleared by /two-factor/disable).

400 error

Bad request (better-auth JSON). Codes: OTP_HAS_EXPIRED (always, with a session or challenge), TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE, VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Codes: INVALID_TWO_FACTOR_COOKIE (no session and no live challenge), INVALID_CODE.

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

Too many requests, with Retry-After. Either ACCOUNT_TEMPORARILY_LOCKED (body AuthAccountLocked; sign-in challenges only) or better-auth's per-IP limiter (3 per 10 seconds on /two-factor/*; body {message}, no code).

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/two-factor/get-totp-uri

Show the authenticator URI again (current password required)

Credentials: App session. Operation id authTwoFactorGetTotpUri.

Session only. Returns the otpauth:// URI of the person's current TOTP secret (to add the factor to another device). Changes nothing.

Request body

Required; application/json.

passwordstringrequired
The person's current password.

Responses

200 success

The URI.

As application/json.

totpURIstringalways present
An otpauth://totp/... URI (6 digits, 30-second period, issuer Data Tactical Group).

400 error

Bad request (better-auth JSON). Codes: TOTP_NOT_ENABLED (no enrolment started), INVALID_PASSWORD (missing or wrong), VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 3 per 10 seconds on /two-factor/*. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/two-factor/generate-backup-codes

Replace the backup codes (current password required)

Credentials: App session. Operation id authTwoFactorGenerateBackupCodes.

Session only, with a live second factor. Generates 10 new backup codes and invalidates every old one. Show them once: they are not retrievable later (/v2/account gives only two_factor.backup_codes_remaining).

Request body

Required; application/json.

passwordstringrequired
The person's current password.

Responses

200 success

The new codes.

As application/json.

statustruealways present
Always true.
backupCodesarray of stringalways present
10 one-time codes, each xxxxx-xxxxx.

400 error

Bad request (better-auth JSON). Codes: TWO_FACTOR_NOT_ENABLED (no live factor), INVALID_PASSWORD (missing or wrong), VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 3 per 10 seconds on /two-factor/*. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

POST /v2/auth/two-factor/disable

Turn the second factor off (current password required)

Credentials: App session. Operation id authTwoFactorDisable.

Session only. Deletes the factor (secret and backup codes), clears twoFactorEnabled, replaces the session with a new one (new cookie), forgets a trusted device, and writes an auth.mfa_disabled audit row when a live factor was turned off. An operator whose factor is off is refused operator access until they enrol again.

Request body

Required; application/json.

passwordstringrequired
The person's current password.

Responses

200 success

Turned off.

As application/json.

statusbooleanalways present
Always true on success.

400 error

Bad request (better-auth JSON). Codes: INVALID_PASSWORD (missing or wrong), VALIDATION_ERROR.

As application/json.

401 error

Unauthorized (better-auth JSON). Code: UNAUTHORIZED (no live session).

As application/json.

403 error

Forbidden (better-auth JSON). Codes: INVALID_ORIGIN / MISSING_OR_NULL_ORIGIN.

As application/json.

429 error

better-auth's request limiter (production): 3 per 10 seconds on /two-factor/*. Body {message}, no code.

As application/json.

Retry-After header
Seconds to wait before retrying.

Generated from taigan/openapi.yaml at catahoula 77925fab712f(API document version 2.0).