Changing your display name
Profile & credentialsThe lightest profile change: one step-up on the password, one call. One field, one endpoint, one step-up token — the retired PATCH /v1/me is mapped in scenario 37.
- POST /v1/me/step-up { action: display_name, currentPassword } → a single-use step-up token. The password alone is enough — no second factor is asked for
- PUT /v1/me/display-name { displayName, stepUpToken } → 200 with the profile, the same shape as GET /v1/me
/v1/me/step-upauthStart a step-up for display_nameProves the CURRENT password and answers { challengeId, stepUpToken, assurance: password, expiresIn, passwordOnlyAllowed, options[], loginChannel? } — both ids are captured and filled into the cards below. The token is single-use and bound to this user, this SESSION, this action and the assurance it reached (STEP_UP_TOKEN_TTL_SECONDS, default 300); spending it on another action, from another session, or twice is 401 step_up_required. passwordOnlyAllowed says whether this password token already suffices; when false, prove one of options[] (a code to a verified, non-young CURRENT value — never a pending one — a TOTP, or a recovery code) for a strong token. Errors: wrong password 401 step_up_failed (counted per user AND per session — 5 in 15 min → 429, and a second throttle trip within 24 h revokes the session); 403 without an acting session (operator / act-as contexts have none); 409 no_password (GET /v1/me → hasPassword: false — set one through the reset flow, scenario 34); 409 change_in_progress when a live change blocks this action (details[0].blocking). For display_name passwordOnlyAllowed is always true and the action is NEVER blocked by a live credential change — unlike every other profile change.
Field guide — what each value means & where it comes from2 fields
Starts a step-up (E-1) for ONE named profile action: proves the CURRENT password and answers 200 { challengeId, stepUpToken, assurance: password, expiresIn, passwordOnlyAllowed, options[], loginChannel? }. Every profile mutation — display name, login mode, email / phone / password change, phone removal, MFA disable, recovery-code regeneration — spends one of these tokens; a live session alone is never enough. Auth + throttled + no-store; the body is .strict(). THE TESTER AUTO-CAPTURES BOTH IDS: challengeId is filled into the …/code and …/verify cards on this page, and stepUpToken into the card that spends it. The token is single-use and bound to this user, this SESSION, this action and the assurance it reached (STEP_UP_TOKEN_TTL_SECONDS, default 300); spending it on another action, from another session, or twice → 401 step_up_required. passwordOnlyAllowed says whether this password token already suffices for the action; when false, continue with …/code (for a code factor) and …/verify for a strong token. options[] are the factors …/verify will accept: a code factor always names the CURRENT value on the account — never a pending one — and only when it is verified and not young (applied with password assurance inside CHANNEL_TRUST_AGE_SECONDS, default 30 days); totp appears when a confirmed authenticator exists, recovery_code while unused codes remain. The challenge lives STEP_UP_CHALLENGE_TTL_SECONDS (default 600) and takes STEP_UP_MAX_ATTEMPTS (default 5) wrong codes. Errors: 401 step_up_failed (wrong password — counted per user AND per session, 5 in 15 min → 429, a second throttle trip within 24 h revokes the session) · 403 without an acting session (operator / act-as contexts have none) · 409 no_password (GET /v1/me → hasPassword: false; set one through the reset flow, scenario 34) · 409 change_in_progress when a live change blocks the action (details[0].blocking lists { kind, status }) · 409 login_channel_unavailable (password_change with no verified loginMode channel). Audited step_up_succeeded / step_up_failed.
actionbodyenumrequiredyou choosedisplay_name— For PUT /v1/me/display-name (scenario 28). passwordOnlyAllowed: true — the password token is enough. Never blocked by a live change.email_change— For POST /v1/me/email-change (scenario 30). passwordOnlyAllowed: true on an account WITHOUT confirmed TOTP; false with one, and for a second contact change beside a strong one. A PASSWORD-assurance change is HELD 3 days after its code and sends notices with a cancel link; a STRONG one applies at once. Blocked (409 change_in_progress) by a live email or password change, and beside a live phone change unless both are strong.phone_change— For POST /v1/me/phone-change (scenario 31, and the link-mode add of scenario 4). Same rules as email_change: password enough without confirmed TOTP; held vs applied by assurance; one live phone change at a time, never beside a password change, beside an email change only when both are strong.password_change— For POST /v1/me/password-change (scenario 35). passwordOnlyAllowed: true. The answer carries loginChannel — the CURRENT loginMode channel the confirmation code will go to, which must be verified (else 409 login_channel_unavailable). Blocked by, and blocks, any live contact change.login_mode— For PUT /v1/me/login-mode (scenario 33). passwordOnlyAllowed: false — needs a STRONG token. Blocked while ANY credential change is live.phone_remove— For DELETE /v1/me/phone (no §2 scenario walks this any more — the doc dropped “Removing a phone credential” at its 2026-09-27 revision, while §3.31 and this action stay live). passwordOnlyAllowed: false — needs a STRONG token. Blocked while ANY credential change is live.mfa_disable— For DELETE /v1/me/mfa (scenario 27). passwordOnlyAllowed: false — needs a STRONG token. Never blocked by a live change.mfa_recovery_codes— For POST /v1/me/mfa/recovery-codes (scenario 27). passwordOnlyAllowed: false — needs a STRONG token. Never blocked by a live change.
currentPasswordbodystringrequiredyou choosePOST https://api.kerja.team/v1/me/step-upResponse guide — what comes back & what each value means12 fields
200 OK, { data } envelope, Cache-Control: no-store. The current password was proven for ONE named action, and this is the PASSWORD-assurance step-up token plus the factors that can raise it to strong. Every profile mutation (display name, email / phone / password change, login mode, phone removal, MFA disable, recovery codes) spends one of these tokens — a live session alone is never enough. The tester captures data.challengeId and data.stepUpToken and fills them into the next cards. 401 step_up_failed = wrong password (5 in 15 min → 429; a second throttle trip in 24 h revokes the session); 409 no_password / change_in_progress / login_channel_unavailable refuse BEFORE a token is minted.
data.challengeIdUUIDalwaysdata.stepUpTokenstring (43-char opaque)alwaysdata.assuranceenumalwayspassword— Always this value here — only the current password has been proven. A strong token comes only from POST /v1/me/step-up/{challengeId}/verify.
data.expiresInnumberalwaysdata.passwordOnlyAllowedbooleanalwaystrue— Spend stepUpToken now: display_name and password_change always; a contact change on an account without confirmed TOTP (that change is then HELD after its code). Raising to strong is still possible and makes a contact change apply at once.false— Password is not enough: login_mode, phone_remove, mfa_disable, mfa_recovery_codes, and a second contact change beside a strong one. Continue with /code (for a code factor) and /verify to get a strong token.
data.optionsarrayalwaysdata.options[].factorenumalwaysemail_code— A code to the verified current email: send it with /code, then /verify.phone_code— A WhatsApp/SMS code to the verified current phone: send it with /code, then /verify.totp— Offered when a confirmed authenticator exists. Nothing to send — go straight to /verify with the app's code.recovery_code— Offered while unused recovery codes remain. Nothing to send — /verify with one; it is consumed.
data.options[].roleenumconditionalcurrent_value— The channel whose value this very action changes (e.g. the current email in an email change) — proving it confirms the owner of the old value.other_channel— The account's OTHER verified channel — proving it shows control of a second, independent credential.channel— The role used for the actions that change no channel value of their own — login_mode, phone_remove and the two MFA actions: every eligible channel is simply offered as a channel. (For phone_remove the phone is still offered as current_value, because that IS the value being removed.)
data.options[].maskedstringconditionaldata.loginChannelobjectconditionaldata.loginChannel.channelenumconditionalemail— The password-change code will be emailed.phone— The password-change code will go to the phone by WhatsApp/SMS.
data.loginChannel.maskedstringconditional/v1/me/display-nameauthSet the display namedisplayName is trimmed, 1–200 chars, NFC-normalized; stepUpToken is a display_name token (password assurance is enough). 200 with the profile. A token minted for a different action, or already spent, is 401 step_up_required. A CHANGED name is projected at once — one user.upserted per app the user holds in ANY of their active tenants — so a display name that is still stale in an app afterwards IS worth investigating (scenario 48). Sending the current name writes and emits nothing. Audited display_name_changed; no notice is sent.
Field guide — what each value means & where it comes from2 fields
Sets the display name (E-4) on a display_name step-up token — password assurance is enough, so the start card's token alone will do. Auth + no-store; .strict(). 200 with the profile (same shape as GET /v1/me). Never blocked by a live change; sending the current name is a no-op that writes and emits nothing. A CHANGED name applies at once and emits one user.upserted per app the user holds in ANY of their active tenants; audited display_name_changed; no notice is sent. Walk-through: scenario 28.
displayNamebodystringrequiredyou choosestepUpTokenbodystringrequiredauto-capturedPUT https://api.kerja.team/v1/me/display-nameResponse guide — what comes back & what each value means24 fields
200 OK, { data } envelope, Cache-Control: no-store — the PROFILE, the same shape as GET /v1/me, with the new displayName. Spends a display_name step-up token (password assurance is enough, scenario 28); never blocked by a live credential change. A changed name applies at once and emits one user.upserted per app the user holds in ANY of their active tenants; sending the current name is a no-op that writes and emits nothing. Audits display_name_changed; no notice is sent. 401 step_up_required = no, spent, foreign or wrong-action token.
data.idUUIDalwaysdata.emailstring | nullalwaysdata.pendingEmailstring | nullalwaysdata.pendingPhonestring | nullalwaysdata.pendingChangesCredentialChange[]alwaysdata.pendingChanges[].idUUIDalwaysdata.pendingChanges[].kindenumalwaysemail— An email change (scenario 30) — the code went to the NEW address; confirm at POST /v1/me/email-change/verify.phone— A phone add or replacement (scenario 31) — the WhatsApp/SMS code went to the NEW number; confirm at POST /v1/me/phone-change/verify.password— A password change (scenario 35) — the code went to the CURRENT value of the loginMode channel; confirm at POST /v1/me/password-change/verify. value is always null.
data.pendingChanges[].valuestring | nullalwaysdata.pendingChanges[].statusenumalwaysawaiting_verification— The code is out and not yet confirmed. Next: the matching …/verify with the code, …/resend for a fresh one (the old code dies), or DELETE to cancel. The current value keeps signing in. Lapses to expired after CONTACT_CHANGE_PENDING_TTL_SECONDS (24 h).held— Contact changes only: the code was confirmed on PASSWORD assurance, so the change waits CONTACT_CHANGE_HOLD_SECONDS (3 days) and worker job J7 applies it at effectiveAt. Every verified current channel was sent contact_change_held with a cancel link (POST /v1/auth/credential-change/cancel); DELETE also still cancels it. The current value keeps signing in until then.applied— In effect: the new email / phone is the verified credential (user.upserted projected across the user's tenants), or the new password is set and every session has ended. Terminal.cancelled— Cancelled by the user (DELETE on the change route, DELETE /v1/me/credential-changes) or through the notice link. The current value was never touched. Terminal.expired— Never confirmed within the pending TTL; closed by J7 with no notice. Terminal — start a new change (and a new step-up) to try again.failed— At verify the target address / number had meanwhile been taken by another account; contact_change_failed was sent. Terminal.
data.pendingChanges[].assuranceenumalwayspassword— Only the current password was proven: a contact change is HELD after its code (3-day window, cancel link sent to every verified channel at request and at hold). A password change needs no more than this.strong— A second factor was proven at POST /v1/me/step-up/{challengeId}/verify: a contact change APPLIES the moment its code is confirmed, and nothing is sent at request.
data.pendingChanges[].requestedAtISO-8601alwaysdata.pendingChanges[].verifiedAtISO-8601 | nullalwaysdata.pendingChanges[].effectiveAtISO-8601 | nullalwaysdata.pendingChanges[].expiresAtISO-8601alwaysdata.pendingChanges[].codeExpiresAtISO-8601 | nullalwaysdata.pendingChanges[].codeSentTostring | nullalwaysdata.displayNamestringalwaysdata.hasMfaEnabledbooleanalwaystrue— MFA is enabled — disabling it or regenerating recovery codes needs a STRONG step-up (scenario 27).false— MFA is not enabled; POST /v1/me/mfa/recovery-codes would answer 409.
data.hasPasswordbooleanalwaystrue— Profile changes are available: start with POST /v1/me/step-up.false— A passwordless (phone-only) account: step-up answers 409 no_password, so send the user through the reset flow (scenario 34) to set one first.
data.emailVerifiedAtISO-8601 | nullalwaysdata.phonestring | nullalwaysdata.phoneVerifiedAtISO-8601 | nullalwaysdata.loginModeenumalwaysemail— Signs in at scenario 9 (email + password); a new-device challenge arrives as an email code.phone— Signs in at scenario 10 (phone + password); a new-device challenge arrives as a phone OTP — which is why DELETE /v1/me/phone answers 409 last_credential until this is switched back to email.
data.foundedTenantIdUUID | nullalways