switching clears the token & responses
No token
31

Switching your primary login channel

Profile & credentials

An account holding BOTH an email and a verified phone chooses which one is its primary channel. That choice decides which sign-in form applies (scenario 9 vs 10) and, more importantly, which channel a new-device challenge is delivered on. Step-up gated.

  1. GET /v1/me → read the current loginMode (email or phone) and confirm both credentials are verified
  2. PATCH /v1/me with { loginMode, currentPassword | otpCode } → audited as login_mode_changed
GET/v1/meauthGet my profile

Current profile: email / phone and their *VerifiedAt stamps (null until verified, null entirely on a single-channel account), hasMfaEnabled, and loginMode — the primary channel that decides which sign-in form applies and which channel a new-device challenge is delivered on. Internal columns (password hash, any staged pending_email) are never returned.

ℹ︎Field guide — what each value means & where it comes fromno inputs

No request body, path params, query params, or caller-set headers. It only needs the authenticated session: the tester sends the captured Authorization access token and the gateway injects x-gateway-context. Returns the current user's profile (id, email, displayName, hasMfaEnabled, emailVerifiedAt).

Bearer token
GET https://api.kerja.team/v1/me
↩︎Response guide — what comes back & what each value means8 fields

200 OK. Body is the standard { data } envelope wrapping the current user's profile object; no notable response headers (no Set-Cookie / Cache-Control). Internal columns such as the password hash are never returned.

data.idUUIDalways
The authenticated user's unique identifier. Use to reference this identity in subsequent calls.
data.emailstringalways
The user's current email address (NFC-normalized). Note it may be unverified; check emailVerifiedAt.
data.displayNamestringalways
The user's human-readable display name. Render in profile/UI.
data.hasMfaEnabledbooleanalways
Whether the user has multi-factor authentication enabled. Drives whether to surface MFA management options.
  • trueMFA is enabled for this user.
  • falseMFA is not enabled for this user.
data.emailVerifiedAtISO-8601always
When the email was verified, or null if not yet. Note an email CHANGE no longer resets this: the new address is staged as pending_email and this stays stamped until the new one is confirmed and swapped in.
data.phonestringalways
The user's E.164 phone, or null on an email-only account. Check phoneVerifiedAt before treating it as a usable channel.
data.phoneVerifiedAtISO-8601always
When the phone was verified, or null. A verified phone is the prerequisite for phone-OTP MFA (scenario 25) and for selecting loginMode: phone (scenario 31).
data.loginModeenumalways
The account's PRIMARY channel — genuinely new information that cannot be derived, because an account holding both credentials is otherwise ambiguous at exactly the point the login screen must be chosen. Set at registration (phone sign-up and phone invitations -> phone; email sign-up and email invitations -> email) and changed only with step-up.
  • emailSigns in at scenario 9 (email + password); a new-device challenge arrives as an email code.
  • phoneSigns in at scenario 10 (phone + password); a new-device challenge arrives as a phone OTP — which is why unlinking the phone requires switching this off first.
PATCH/v1/meauthSwitch primary channel (step-up)

STEP-UP IS MANDATORY — a live session is not enough. Flipping the channel changes which challenge a future attacker faces, so an attacker holding a stolen session could otherwise redirect the new-device code to a channel they control. The TARGET CHANNEL MUST BE USABLE: switching to phone without a verified phone (or to email without a verified email) is refused with 409 conflict, otherwise a user could lock themselves out of their own new-device challenge with no way back in. It is a no-op — and needs no step-up — when the mode is already what you asked for. login_mode is set at registration (scenario 3 → phone; scenario 2 and every invitation → email). Changing it does NOT invalidate sessions or device trust.

⛓ Needs an out-of-band value?
Request body (JSON)
ℹ︎Field guide — what each value means & where it comes from5 fields

Updates the current user's own profile. Auth only (gateway context handles Authorization / x-gateway-context). Body is .strict(), which blocks mass assignment — status, roles and the like are not settable here — and at least one updatable field must be present. Two very different edits live on this route: an EMAIL change, which is STAGED (the current verified address keeps working until the new one is confirmed), and a LOGIN MODE change, which is step-up gated.

displayNamebodystringconditionalyou choose
The user freely types their new display name. Required only if email is omitted; at least one of displayName/email must be present. Trimmed, 1-200 chars, NFC-normalized.
emailbodystringconditionalyou choose
The user freely types their new email address. Trimmed, valid email, <=320 chars, NFC-normalized. The change is STAGED, not applied: the address is stored as pending_email and a verify challenge (24h link + 15 min code) is issued to it, while email / emailVerifiedAt / status stay UNCHANGED — so the account is never locked out by a typo and the 200 reflects the current, unchanged address. It becomes the active verified email only when confirmed at POST /v1/auth/email/verify. 409 conflict if it already belongs to another active identity.
loginModebodyenumconditionalyou choose
The account's PRIMARY channel — which sign-in form applies and, more importantly, which channel a new-device challenge is delivered on. Requires step-up (currentPassword or otpCode) and a verified target channel. It is a no-op, needing no step-up, when the mode is already what you asked for. Set at registration (phone sign-up -> phone; email sign-up and every invitation -> email) and changing it does NOT invalidate sessions or device trust.
  • emailSign in with email + password (scenario 9); a new-device challenge arrives as an email code (device_email_code). Refused 409 without a verified email.
  • phoneSign in with the E.164 number + password (scenario 10); a new-device challenge arrives as a phone OTP (device_phone_otp). Refused 409 without a verified phone.
currentPasswordbodystringconditionalyou choose
Step-up factor for a loginMode change, for a password-holding account. Mandatory because flipping the channel changes which challenge a future attacker faces — an attacker holding a stolen session could otherwise redirect the new-device code to a channel they control. A wrong factor -> 401.
otpCodebodystringconditionalout-of-band
Step-up factor for a loginMode change, for a PASSWORD-LESS (phone-only) account: a step_up-purpose OTP obtained from POST /v1/me/mfa/otp/request. Supply currentPassword OR otpCode.
Bearer token
PATCH https://api.kerja.team/v1/me
↩︎Response guide — what comes back & what each value means5 fields

200 OK returning the profile in the standard { data } envelope (same shape as GET /v1/me). READ IT CAREFULLY AFTER AN EMAIL CHANGE: the change is STAGED, so email and emailVerifiedAt still show the CURRENT, unchanged, still-working address — the staged pending_email is deliberately not exposed. It becomes the active verified address only when confirmed at POST /v1/auth/email/verify, which is why a typo'd change is harmless. A loginMode change, by contrast, is applied immediately (audited as login_mode_changed) and is visible in data.loginMode; it invalidates neither sessions nor device trust.

data.idUUIDalways
The authenticated user's unique identifier. Stable across the profile update; use it to correlate the caller's identity in follow-up calls.
data.emailstringalways
The user's current email address, reflecting the update if email was changed. Changing it sets the account unverified until re-verified via the email-verification flow.
data.displayNamestringalways
The user's display name, reflecting the update if displayName was changed. Show this in the tester as the updated profile name.
data.hasMfaEnabledbooleanalways
Whether the user has a verified MFA factor enrolled. Not settable via this endpoint; echoed from the current account state.
  • trueUser has MFA enabled; step-up/MFA challenges apply on sensitive flows.
  • falseUser has no MFA factor enrolled.
data.emailVerifiedAtISO-8601always
Timestamp when the email was verified, or null if unverified. Changing the email via this call resets it to null (a new single-use 24h verify token is issued out-of-band), so the client should prompt the user to re-verify.