Migrating a client off retired routes
Legacy clients§153 split profile editing into one route per field, each spending a step-up token. The old routes stay mounted and answer 410 gone — with a message naming the successor — so an old client fails loudly instead of half-working. A few older sign-in routes are DEPRECATED BUT STILL WORK. This is the map for a client that still calls either kind; every 410 card below should answer 410 (or 403 without a gateway context), never 200.
- PATCH /v1/me → 410. Name → PUT /v1/me/display-name (scenario 28); email → POST /v1/me/email-change + verify (scenario 30); login mode → PUT /v1/me/login-mode (scenario 33)
- POST /v1/auth/password/change → 410. → step-up + POST /v1/me/password-change + verify (scenario 35)
- POST /v1/me/phone/otp/request · POST /v1/me/phone → 410. → POST /v1/me/phone-change + verify (scenario 31)
- POST /v1/me/mfa/otp/request → 410. A step-up code is sent by POST /v1/me/step-up/{challengeId}/code, bound to the challenge that will spend it (scenario 27)
- POST /v1/auth/phone/register → deprecated, still 200 — the one-shot password-less phone sign-up; new clients use registration/phone/verify + complete (scenario 3)
- POST /v1/auth/mfa/otp/request · POST /v1/auth/mfa/verify → deprecated, still working — the single-hop MFA challenge; new clients follow the chain at login/challenge/verify + resend (scenario 12)
/v1/meauthRetired — PATCH /v1/me (expect 410)410 gone, and the message names the successors: displayName → PUT /v1/me/display-name, email → POST /v1/me/email-change + /verify, loginMode → PUT /v1/me/login-mode. DO NOT RETRY A 410 — it has no side effect and will never succeed. Without a gateway context the same route answers 403 first. The successors differ in SHAPE, not only in path: the old body carried the current password inline; every successor first needs a step-up token, and most send a code you must confirm.
Field guide — what each value means & where it comes from5 fields
RETIRED (§153 X-2) — answers 410 gone for every caller (403 forbidden first without a gateway context); the message names the successors. The body below is documented only so an old client can map it: the route answers 410 WITHOUT reading it, so nothing is applied, staged or validated. One field, one endpoint, one step-up token now: displayName → PUT /v1/me/display-name (scenario 28); email → POST /v1/me/email-change + POST /v1/me/email-change/verify (scenario 30); loginMode → PUT /v1/me/login-mode (scenario 33). The successors differ in SHAPE, not only path — each first needs a step-up token from POST /v1/me/step-up. Do not retry a 410 (scenario 37).
displayNamebodystringoptionalyou chooseemailbodystringoptionalyou chooseloginModebodyenumoptionalyou chooseemail— Ignored here (410). At PUT /v1/me/login-mode: sign in with email + password; a new-device challenge arrives as an email code.phone— Ignored here (410). At PUT /v1/me/login-mode: sign in with the E.164 number + password; a new-device challenge arrives as a phone OTP.
currentPasswordbodystringoptionalyou chooseotpCodebodystringoptionalout-of-bandPATCH https://api.kerja.team/v1/meResponse guide — what comes back & what each value meansno body
RETIRED (§153). Every call answers 410 gone with a message naming the successor — one field, one endpoint, one step-up token: displayName → PUT /v1/me/display-name (scenario 28); email → POST /v1/me/email-change + /verify (scenario 30); loginMode → PUT /v1/me/login-mode (scenario 33). Scenario 37 exercises the 410. 403 forbidden instead when the request carries no gateway context. There is no success shape any more.
/v1/auth/password/changeauthRetired — POST /v1/auth/password/change (expect 410)410 gone for every caller. A password is now changed in steps that never carry the current password in a body: POST /v1/me/step-up proves it and mints a password_change token, POST /v1/me/password-change submits the new password and sends a code to the sign-in channel, POST /v1/me/password-change/verify confirms it and ends every session (scenario 35).
Field guide — what each value means & where it comes from2 fields
RETIRED (§153 X-5) — answers 410 gone for every caller (403 forbidden first without a gateway context); the message names the successor. The body below is documented only so an old client can map it: the route answers 410 WITHOUT reading it, so no password is checked or changed and no session is revoked. A password is now changed in steps that never carry the current password in a change body: POST /v1/me/step-up { action: password_change, currentPassword } proves it and mints a token → POST /v1/me/password-change { newPassword, stepUpToken } holds the new password and sends a code to the loginMode channel → POST /v1/me/password-change/verify { code } applies it and ends every session (scenario 35). Do not retry a 410 (scenario 37).
currentPasswordbodystringoptionalyou choosenewPasswordbodystringoptionalyou choosePOST https://api.kerja.team/v1/auth/password/changeResponse guide — what comes back & what each value meansno body
RETIRED (§153). Every call answers 410 gone with a message naming the successor — a password is now changed in steps that never carry the current password in a body: POST /v1/me/step-up (action password_change) proves it, POST /v1/me/password-change submits the new one and sends a code to the loginMode channel, POST /v1/me/password-change/verify confirms it and ends every session (scenario 35). 403 forbidden instead when the request carries no gateway context. There is no success shape any more.
/v1/me/phone/otp/requestauthRetired — POST /v1/me/phone/otp/request (expect 410)410 gone. A phone is added or replaced through POST /v1/me/phone-change, which sends the code to the new number itself (scenario 31).
Field guide — what each value means & where it comes from1 field
RETIRED (§153 X-3) — answers 410 gone for every caller (403 forbidden first without a gateway context); the message names the successor. A phone is now added or replaced through POST /v1/me/phone-change { phone, stepUpToken }, which sends the code to the new number itself — there is no separate OTP-request step any more (scenario 31). Do not retry a 410 (scenario 37).
phonebodyE.164optionalyou choosePOST https://api.kerja.team/v1/me/phone/otp/requestResponse guide — what comes back & what each value meansno body
RETIRED (§153). Every call answers 410 gone with a message naming the successor — POST /v1/me/phone-change, which sends the code to the new number itself (scenario 31). 403 forbidden instead when the request carries no gateway context. There is no success shape any more.
/v1/me/phoneauthRetired — POST /v1/me/phone (expect 410)410 gone. The code sent by POST /v1/me/phone-change is confirmed at POST /v1/me/phone-change/verify. Adding a phone is no longer OTP-only: it spends a step-up token like every other credential change.
Field guide — what each value means & where it comes from2 fields
RETIRED (§153 X-3) — answers 410 gone for every caller (403 forbidden first when there is no gateway context); the message names the successor. The old OTP-only link — { phone, code } with no step-up — no longer exists: adding or replacing a phone now spends a step-up token like every other credential change. Successor: POST /v1/me/phone-change { phone, stepUpToken } (which sends the code to the new number itself) + POST /v1/me/phone-change/verify { code } — scenario 31. Do not retry a 410; it has no side effect and never succeeds (scenario 37).
phonebodyE.164optionalyou choosecodebodystringoptionalout-of-bandPOST https://api.kerja.team/v1/me/phoneResponse guide — what comes back & what each value meansno body
RETIRED (§153). Every call answers 410 gone with a message naming the successor — POST /v1/me/phone-change/verify, which confirms the code POST /v1/me/phone-change sent. Adding a phone is no longer OTP-only: it spends a phone_change step-up token like every other credential change (scenario 31). 403 forbidden instead when the request carries no gateway context. There is no success shape any more.
/v1/me/mfa/otp/requestauthRetired — POST /v1/me/mfa/otp/request (expect 410)410 gone. A step-up code is now sent by POST /v1/me/step-up/{challengeId}/code, bound to the challenge that will spend it — an unbound OTP could be replayed into a different action (scenario 27).
Field guide — what each value means & where it comes fromno inputs
RETIRED (§153 X-8) — answers 410 gone for every caller (403 forbidden first without a gateway context). It took no body; it used to text a step_up-purpose OTP to the caller's verified phone for the old otpCode factor. That factor no longer exists: a step-up code is sent by POST /v1/me/step-up/{challengeId}/code, bound to the challenge that will spend it (scenario 27). Do not retry a 410 (scenario 37).
POST https://api.kerja.team/v1/me/mfa/otp/requestResponse guide — what comes back & what each value meansno body
RETIRED (§153). Every call answers 410 gone with a message naming the successor — POST /v1/me/step-up/{challengeId}/code, which sends a step-up code bound to the challenge that will spend it (scenario 27). 403 forbidden instead when the request carries no gateway context. There is no success shape any more.
/v1/auth/phone/registerpublicDeprecated, still 200 — POST /v1/auth/phone/registerDEPRECATED IS NOT RETIRED — this runs on the same engine as its successor and is kept so existing clients do not break. The pre-Enhancement-4 one-shot that creates a PASSWORD-LESS account (user + tenant + founding admin) in a single call and returns tokens (amr: [sms]). New clients use POST /v1/auth/registration/phone/verify + /complete (scenario 3). Policy-gated, and refused 403 registration_combination_required under combinationMode=atomic.
Field guide — what each value means & where it comes from5 fields
DEPRECATED BUT FULLY WORKING (scenario 37): the pre-Enhancement-4 one-shot that creates a PASSWORD-LESS phone-only account and its own tenant in one call (200, tokens with amr [sms]). New clients use the three-step path — phone/otp/request → registration/phone/verify → registration/complete — which ends in a real password (scenario 3). Public phone sign-up — no Authorization needed. The body is .strict(): only the five listed fields are accepted. You must already have completed an OTP-request step and obtained the SMS code out-of-band.
phonebodyE.164requiredyou choosecodebodystringrequiredout-of-banddisplayNamebodystringoptionalyou chooseorganizationNamebodystringoptionalyou choosetrustDevicebodybooleanoptionalyou choosePOST https://api.kerja.team/v1/auth/phone/registerResponse guide — what comes back & what each value means9 fields
200 OK with the standard { data } envelope; issues an inline session for the new phone-only user. Response headers: Set-Cookie: refresh_token (httpOnly), Set-Cookie: device_token (httpOnly) unless trustDevice: false was sent, and Cache-Control: no-store. The refresh token is delivered only via cookie, not in the body.
data.accessTokenJWTalwaysdata.tokenTypeenumalwaysBearer— Token is sent in the Authorization header as 'Bearer <accessToken>'.
data.expiresInintegeralwaysdata.userobjectalwaysdata.user.idUUIDalwaysdata.user.phonestringalwaysdata.user.displayNamestringconditionaldata.user.tenantIdUUIDalwaysSet-Cookie: device_tokencookieconditional/v1/auth/mfa/otp/requestpublicDeprecated, still working — POST /v1/auth/mfa/otp/requestThe legacy single-hop MFA challenge (phone_otp only): submit the mfaToken from a legacy-shape login ({ mfaRequired, method, mfaToken } — DEVICE_TRUST_ENABLED off) → 202 and an OTP to the masked phoneHint. Rewired onto the same engine as the chain. New clients use POST /v1/auth/login/challenge/verify + /resend (scenario 12), which can also express a chain.
Field guide — what each value means & where it comes from1 field
DEPRECATED, STILL WORKING (scenario 37): the legacy single-hop MFA challenge, returned by login only when DEVICE_TRUST_ENABLED is off; new clients follow the challenge chain (POST /v1/auth/login/challenge/verify, scenario 12). JSON body with one field — the mfaToken from the login MFA challenge. No access token exists yet; the mfaToken is the only credential. This applies to phone_otp challenges only (a totp challenge needs no send). 202; re-calling resends and replaces the live challenge (throttled → 429). No verified phone on file → 409 no_verified_phone.
mfaTokenbodystringrequiredfrom a responsePOST https://api.kerja.team/v1/auth/mfa/otp/requestResponse guide — what comes back & what each value means1 field
202 Accepted, { data } envelope, Cache-Control: no-store, when the MFA OTP was sent (or resent — re-calling replaces the live code). The masked destination was already given as phoneHint in the login response. Not uniform: a bad / expired mfaToken is 401 and an account with no verified phone on file is 409 no_verified_phone (reveal).
data.statusenumalwaysaccepted— The OTP request was accepted for delivery over SMS or WhatsApp (channel chosen downstream). The only value on a 202.
/v1/auth/mfa/verifypublicVerify MFA challenge (deprecated single-hop)DEPRECATED but fully working — rewired onto the same engine as the challenge chain with a one-element plan and the same acceptance order (TOTP → recovery code → phone OTP). code is a phone OTP, a TOTP code, or a single-use recovery code. Success auto-captures data.accessToken; amr becomes [pwd, mfa]. New clients should use POST /v1/auth/login/challenge/verify (scenario 12/13), which can also express a chain.
Field guide — what each value means & where it comes from2 fields
DEPRECATED, STILL WORKING (scenario 37): completes the legacy single-hop MFA challenge; new clients follow the chain at POST /v1/auth/login/challenge/verify (scenario 12). Two body fields; no Authorization header — the mfaToken issued by login is the credential. Sent with Cache-Control no-store. On success the full session (amr [pwd, mfa]) — or, for an account that belongs to no organization, the noOrganization / invitationPending body of POST /v1/auth/login (scenario 15 / 52).
mfaTokenbodystringrequiredfrom a responsecodebodystringrequiredauthenticatorPOST https://api.kerja.team/v1/auth/mfa/verifyResponse guide — what comes back & what each value means15 fields
200 OK with a { data } envelope and Cache-Control: no-store. Normally issues the full session (same shape as a non-MFA login): Set-Cookie refresh_token (httpOnly) plus the access token and user in the body; amr is [pwd, mfa] whichever factor (phone OTP, TOTP, recovery code — the latter consumed) was used. It can also end in one of the two SESSIONLESS 200s of §3.6 when the chain clears for an account that belongs nowhere — { noOrganization, bootstrapToken, expiresIn } (scenario 15) or { invitationPending, acceptToken, expiresIn, invitations[] } (scenario 52): neither mints a session row, a refresh or device cookie, an access token or a tenant_id claim. READ the body; a 200 alone is not "signed in".
data.accessTokenstring (JWT)conditionaldata.tokenTypeenumconditionalBearer— Send the access token as Authorization: Bearer <accessToken>.
data.expiresInnumberconditionaldata.user.idUUIDconditionaldata.user.emailstringconditionaldata.user.displayNamestringconditionaldata.user.tenantIdUUIDconditionalSet-Cookie: refresh_tokencookieconditionaldata.noOrganizationbooleanconditionaltrue— Carry bootstrapToken to POST /v1/auth/organizations/bootstrap to found an organization and receive the session there.
data.bootstrapTokenstringconditionaldata.invitationPendingbooleanconditionaltrue— Carry acceptToken to POST /v1/auth/invitations/accept (with the invitation link or code) — that call activates the seat and ends in a session.
data.acceptTokenstringconditionaldata.invitationsarrayconditionaldata.invitations[].tenantIdUUIDconditionaldata.invitations[].tenantNamestringconditional