Switching your primary login channel
Profile & credentialsAn 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.
- GET /v1/me → read the current loginMode (email or phone) and confirm both credentials are verified
- PATCH /v1/me with { loginMode, currentPassword | otpCode } → audited as login_mode_changed
/v1/meauthGet my profileCurrent 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).
GET https://api.kerja.team/v1/meResponse 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.idUUIDalwaysdata.emailstringalwaysdata.displayNamestringalwaysdata.hasMfaEnabledbooleanalwaystrue— MFA is enabled for this user.false— MFA is not enabled for this user.
data.emailVerifiedAtISO-8601alwaysdata.phonestringalwaysdata.phoneVerifiedAtISO-8601alwaysdata.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 unlinking the phone requires switching this off first.
/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.
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 chooseemailbodystringconditionalyou chooseloginModebodyenumconditionalyou chooseemail— Sign in with email + password (scenario 9); a new-device challenge arrives as an email code (device_email_code). Refused 409 without a verified email.phone— Sign 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 chooseotpCodebodystringconditionalout-of-bandPATCH https://api.kerja.team/v1/meResponse 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.idUUIDalwaysdata.emailstringalwaysdata.displayNamestringalwaysdata.hasMfaEnabledbooleanalwaystrue— User has MFA enabled; step-up/MFA challenges apply on sensitive flows.false— User has no MFA factor enrolled.
data.emailVerifiedAtISO-8601always