Adding a phone credential
Profile & credentialsA signed-in user links a phone as a login factor. OTP-ONLY — the phone OTP is the possession proof and no password step-up is required. Counterpart to removing a phone (scenario 30), which does require step-up.
- GET /v1/me → review the current profile, including loginMode, before changing credentials
- POST /v1/me/phone/otp/request → 202, an OTP is sent to the new number (purpose=link)
- POST /v1/me/phone → phone + code → sets phone + phoneVerifiedAt
/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/me/phone/otp/requestauthRequest phone-link OTPpurpose=link. Always 202. NOT policy-gated — /v1/me credential management is independent of the sign-up policy. 409 conflict if the phone already belongs to another live user (numbers are globally unique).
Field guide — what each value means & where it comes from1 field
Begins a phone-link challenge (purpose=link) for the authenticated caller; always returns 202. Beyond the tester-supplied Authorization and x-gateway-context, the only caller input is the phone number in the body. No Idempotency-Key or step-up is required for this request step.
phonebodyE.164requiredyou choosePOST https://api.kerja.team/v1/me/phone/otp/requestResponse guide — what comes back & what each value means1 field
Always 202 Accepted with the standard { data } envelope; the body is a fixed acknowledgement { data: { status: "accepted" } } carrying no OTP material. No notable response headers (no Set-Cookie, no Cache-Control: no-store).
data.statusenumalwaysaccepted— The challenge was accepted and the OTP dispatched out-of-band; this is the only value ever returned on a 202 success.
/v1/me/phoneauthVerify OTP & set phoneOTP-only (no password step-up); the body is .strict() so any extra field (e.g. currentPassword) → 422. This asymmetry with REMOVAL (scenario 30, which does require step-up) is deliberate. A linked, verified phone unlocks phone-OTP MFA (scenario 25) and makes loginMode: phone selectable (scenario 31) — but adding it changes neither on its own.
Field guide — what each value means & where it comes from2 fields
Verifies the SMS OTP and sets the caller's phone (phoneVerifiedAt), enforcing global phone uniqueness. Auth required (the tester + gateway supply Authorization / x-gateway-context). OTP-only (no password step-up): the phone OTP is the sole proof of possession — a deliberate deviation from the step-up-on-credential-change rule (AUTH-IAM-01) by product decision. JSON body is .strict(), so any extra field (e.g. currentPassword) → 422. No Idempotency-Key required.
phonebodyE.164requiredyou choosecodebodystringrequiredout-of-bandPOST https://api.kerja.team/v1/me/phoneResponse guide — what comes back & what each value means3 fields
200 OK with the standard { data } envelope; data is the updated user profile after the OTP is verified and the phone is linked. Response carries Cache-Control: no-store; no Set-Cookie. No new tokens are issued.
data.idUUIDalwaysdata.phonestringalwaysdata.phoneVerifiedAtISO-8601always