Resetting a forgotten password
PasswordsStart a reset from an email OR an E.164 phone (BR-8), redeem the emailed token or the SMS code to set a new password, then log in. The reset revokes every session and every device trust — and then re-trusts the browser that performed it (BR-9), so this machine is not challenged afterwards.
- POST /v1/auth/password/forgot → identifier = an email OR an E.164 phone (BR-8). Email → a 1h reset token; phone → a 5-min reset-purpose SMS OTP. Unknown → 404 account_not_found / phone_not_registered
- POST /v1/auth/password/reset → submit the emailed { token } OR { phone, code } + newPassword. Password replaced; ALL sessions revoked and ALL device trust revoked — then THIS browser is re-trusted from the proof it just gave (BR-9)
- POST /v1/auth/login → log in with the new password (no new-device challenge on this browser unless you sent trustDevice: false)
/v1/auth/password/forgotpublicForgot password (email or phone)identifier is an email OR an E.164 phone, discriminated by SHAPE exactly as login is (BR-8); { email } is still accepted as a deprecated alias. Email → a password_reset token (1h TTL) out-of-band; phone → a reset-PURPOSE 6-digit OTP (~5 min), a distinct purpose so it can never satisfy a login / mfa / link / device challenge. REVEALS whether the identifier is registered, and does so symmetrically: unknown email → 404 account_not_found, unknown phone → 404 phone_not_registered — the phone case is deliberately not a quieter oracle than the email one. Audited as password_reset_requested. A phone-only account CAN reset here (that is what BR-8 added); it can also simply sign in with a one-time code instead (scenario 14).
Field guide — what each value means & where it comes from2 fields
Public, throttled, no auth. Since BR-8 it takes an IDENTIFIER — an email OR an E.164 phone, discriminated by shape exactly as login is — and the two routes diverge from there: email mints a password_reset token (1h TTL) delivered out-of-band, phone mints a reset-PURPOSE 6-digit OTP (~5 min) into identity.otp_challenges, a distinct purpose so it can never satisfy a login / mfa / link / device challenge. It does NOT anonymise a miss: an unknown identifier is disclosed either way — unknown email is 404 account_not_found, unknown phone is 404 phone_not_registered — so the phone case is deliberately not a quieter oracle than the email one. Audited as password_reset_requested.
identifierbodystringconditionalyou chooseemailbodystringconditionalyou choosePOST https://api.kerja.team/v1/auth/password/forgotResponse guide — what comes back & what each value meansno body
200 OK with the standard { data } envelope, where data is an empty object ({ "data": {} }) — there are no body properties to consume. THE 200 ITSELF IS THE INFORMATION: this endpoint deliberately REVEALS whether the identifier is registered rather than answering uniformly, and does so symmetrically — an unknown email is 404 account_not_found and an unknown phone is 404 phone_not_registered, so the phone case is not a quieter oracle than the email one. A 200 therefore means the account exists and the credential went out on its channel: the email route minted a single-use password_reset token (1h TTL), the phone route a reset-purpose SMS OTP (~5 min) whose distinct purpose keeps it from satisfying a login / mfa / link / device challenge. Audited as password_reset_requested. No Set-Cookie; no auth tokens returned.
/v1/auth/password/resetpublicReset password (token, or phone + code)EXACTLY ONE credential: { token } (the emailed link, single-use) OR { phone, code } (the SMS OTP, BR-8) — both together is 422, and a bad token and a bad/expired/exhausted code are the same uniform 401. Replaces the password, revokes ALL sessions, and revokes ALL device trust including this browser's old device_token. THEN IT RE-TRUSTS THIS BROWSER (BR-9): the reset itself was a fresh proof of the channel, so a new device_token cookie is set and the next login here is not challenged. Every OTHER device stays untrusted, and the old cookie is never honoured — the trust is re-earned, not preserved. Send trustDevice: false on a shared computer to decline it, or when DEVICE_TRUST_ENABLED is off nothing is granted either way. (§2.23's gotcha list still says the resetting machine is challenged too; §3.11 is the newer and more specific text, and BR-9 is what the service implements.)
Field guide — what each value means & where it comes from5 fields
Public, throttled endpoint — no Authorization or gateway context needed. The .strict() body carries EXACTLY ONE credential — the emailed { token }, or { phone, code } from the SMS (BR-8) — plus the new password. Success replaces the password hash, revokes ALL of the user's sessions and revokes ALL device trust, then RE-TRUSTS THE BROWSER THAT PERFORMED THE RESET from the proof it just gave (BR-9), setting a fresh device_token cookie (HttpOnly, Path=/v1/auth) that is never in the body. Every OTHER device stays untrusted and the old cookie is never honoured. A bad token and a wrong / expired / exhausted code are the same uniform 401.
tokenbodystringrequiredout-of-bandphonebodystringconditionalyou choosecodebodystringconditionalout-of-bandtrustDevicebodybooleanoptionalyou choosenewPasswordbodystringrequiredyou choosePOST https://api.kerja.team/v1/auth/password/resetResponse guide — what comes back & what each value means1 field
200 OK with the standard { data } envelope where data is an empty object ({ "data": {} }) — there is no body payload to consume; success simply confirms the reset completed. Everything that matters here is a side effect: the credential is consumed (the token is single-use, the SMS code likewise), the password hash is replaced, ALL of the user's sessions are revoked, and ALL device trust is revoked — including this browser's old device_token. THEN BR-9 RE-TRUSTS THIS BROWSER: unless trustDevice: false was sent (or DEVICE_TRUST_ENABLED is off), a FRESH device_token arrives as Set-Cookie (HttpOnly, Path=/v1/auth) minted from the proof the reset itself just performed, so the next login here raises no new-device challenge while every other device stays untrusted. The token value is never in the body. Audited as password_reset.
Set-Cookie: device_tokencookieconditional/v1/auth/loginpublicLog in (identifier + password)identifier is an email OR an E.164 phone — the shape decides the lookup ({ email } is still accepted as a deprecated alias) — and it is OPTIONAL on a recognised device, where the device_token cookie names the account and the body needs only { password }. Three possible answers: { accessToken, … } (nothing left to prove — auto-captured as the active bearer), { challengeToken, challenge, remaining } (a device and/or MFA hop is pending — continue at scenario 11/12/13), or the legacy { mfaRequired, method, mfaToken } shape when DEVICE_TRUST_ENABLED is off (scenario 12). Gated by the LOGIN policy before the credential is even checked → 403 login_method_disabled (scenario 7).
Field guide — what each value means & where it comes from4 fields
Public, unauthenticated login. No Authorization header; send only the JSON body below. Throttled per credential/IP; response is no-store. The answer is one of three shapes — tokens, a challenge chain, or the legacy MFA challenge — and which one you get is NOT checkable beforehand (there is no pre-login MFA-status endpoint, by anti-enumeration design). Before the credential is examined at all, the active LOGIN policy is checked: if the channel this attempt uses is off, the answer is 403 login_method_disabled for a right and a wrong password alike (which discloses nothing — GET /v1/auth/login-policy publishes the same fact unauthenticated). Read that policy first in scenario 7.
identifierbodystringconditionalyou chooseemailbodystringconditionalyou chooseclientIdbodystringoptionalyou choosepasswordbodystringrequiredyou choosePOST https://api.kerja.team/v1/auth/loginResponse guide — what comes back & what each value means16 fields
200 OK, { data } envelope, Cache-Control: no-store. THREE possible shapes, and the login response itself is the only way to learn which branch applies (there is no pre-login MFA-status endpoint, by anti-enumeration design): (1) tokens — nothing more to prove; (2) a CHALLENGE CHAIN { challengeToken, challenge, remaining } when a device and/or MFA hop is pending, continued at POST /v1/auth/login/challenge/verify; (3) the LEGACY single-hop { mfaRequired, method, phoneHint, mfaToken } when DEVICE_TRUST_ENABLED is off. Only the token branch sets Set-Cookie: refresh_token.
data.accessTokenstringconditionaldata.tokenTypestringconditionalBearer— Send the accessToken as 'Authorization: Bearer <accessToken>'.
data.expiresInnumberconditionaldata.user.idstringconditionaldata.user.emailstringconditionaldata.user.displayNamestringconditionaldata.user.tenantIdstringconditionaldata.challengeTokenstringconditionaldata.challenge.typeenumconditionaldevice_email_code— New browser, email-mode account (LN2) — a code was emailed. Continue at scenario 11.device_phone_otp— New browser, phone-mode account (LN1) — an OTP went out on the account's primary channel.mfa_phone_otp— An enrolled phone-OTP second factor (LE3) — the OTP is sent as the challenge is issued. A recognised device never skips this hop.mfa_totp— An enrolled authenticator app — nothing is sent; the code comes from the app, so no resend applies.
data.challenge.hintstringconditionaldata.challenge.expiresInnumberconditionaldata.remainingnumberconditionaldata.methodenumconditionalphone_otp— Send/resend the OTP with POST /v1/auth/mfa/otp/request, then verify. phone_otp is the method advertised when both are confirmed.totp— Read the code from the authenticator app — there is nothing to send, so skip the otp/request step.
data.phoneHintstringconditionaldata.mfaRequiredbooleanconditionaltrue— MFA challenge pending; proceed to /v1/auth/mfa/verify carrying mfaToken. No access token or refresh cookie issued yet.
data.mfaTokenstringconditional