switching clears the token & responses
No token
29

Adding a phone credential

Profile & credentials

A 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.

  1. GET /v1/me → review the current profile, including loginMode, before changing credentials
  2. POST /v1/me/phone/otp/request → 202, an OTP is sent to the new number (purpose=link)
  3. POST /v1/me/phone → phone + code → sets phone + phoneVerifiedAt
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.
POST/v1/me/phone/otp/requestauthRequest phone-link OTP

purpose=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).

Request body (JSON)
ℹ︎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 choose
The phone number to link, typed by the user in E.164 format (e.g. +15551234567). The server sends an OTP to this number to begin the link challenge; it must match the phone later submitted to POST /v1/me/phone.
Bearer token
POST https://api.kerja.team/v1/me/phone/otp/request
↩︎Response 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.statusenumalways
Fixed acknowledgement that the phone-link OTP challenge (purpose=link) was accepted and an otp.requested event emitted. The actual code is delivered out-of-band (SMS); the client proceeds to POST /v1/me/phone with the received code.
  • acceptedThe challenge was accepted and the OTP dispatched out-of-band; this is the only value ever returned on a 202 success.
POST/v1/me/phoneauthVerify OTP & set phone

OTP-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.

⛓ Needs an out-of-band value?
Request body (JSON)
ℹ︎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 choose
The phone number the user is linking, in E.164 format. Must match the number used in the prior §3.29 OTP challenge. The user types this.
codebodystringrequiredout-of-band
The numeric one-time OTP the user received via SMS on the new phone. Obtained from the SMS side channel and pasted in; it proves possession of the new phone.
Bearer token
POST https://api.kerja.team/v1/me/phone
↩︎Response 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.idUUIDalways
The caller's user id (the authenticated subject). Confirms which user record was updated.
data.phonestringalways
The newly linked phone number in E.164 format (matches the §3.19 challenge phone). Now usable as a login factor.
data.phoneVerifiedAtISO-8601always
Timestamp when the OTP was verified and the phone became confirmed. Non-null indicates the phone is verified.