Resuming an interrupted sign-up
Signing upA user closed the tab, lost the SMS, or came back days later. They re-enter only the email or phone they started with, and the API says which step to render — for every sign-up path above and for a pending invitation.
- POST /v1/auth/registration/resume with { email } OR { phone } (exactly one) → { state, method, maskedDestination, nextStep, canResend, expiresAt }
- Continue at the endpoint named in nextStep: not_started → register / phone·otp·request · awaiting_phone_otp → registration/phone/verify · awaiting_password → phone·otp·request then registration/complete · awaiting_email_code → email/verify · awaiting_invitation → invitations/accept · complete → login
/v1/auth/registration/resumepublicWhere did I stop?Exactly one of { email } / { phone }. The six states are DERIVED from facts that already exist (users.status, password_hash, the *_verified_at columns, live tokens, memberships) — never from a duplicate state table. Resume is NOT a credential: it returns where you are, never a token, a code, or a password reset — which is why awaiting_password sends you back through a fresh OTP instead of re-issuing the registrationToken. Only maskedDestination comes back (b••••@example.com, +••••••6789). awaiting_email_code is reported even when the code has LAPSED, with canResend: true. Precedence: a finished account beats a stale token; a pending invitation beats a half-finished self sign-up; an active membership beats an invited one. This is an enumeration oracle by design (same posture as register's 409) and is throttled per IP. Tip: the OOB registration-token helper seeds the awaiting_password state — it creates a phone-verified, password-less identity with no tenant.
Field guide — what each value means & where it comes from2 fields
Public, throttled per IP. Submit EXACTLY ONE of email / phone — the identifier the user started with is all they have to remember — and the resolver answers with the step to render. It never returns a credential: no token, no code, no password reset. Deliberately an enumeration oracle (the same posture register's 409 email_already_registered already takes); CAPTCHA/bot defence remains a production prerequisite for this surface.
emailbodystringconditionalyou choosephonebodystringconditionalyou choosePOST https://api.kerja.team/v1/auth/registration/resumeResponse guide — what comes back & what each value means6 fields
200 with the resolver's answer. It tells you WHERE YOU ARE — never a token, a code, or a password reset. The state is DERIVED from facts that already exist (users.status, password_hash, the *_verified_at columns, live tokens, memberships), never from a duplicate state table which would be a second truth and the first thing to drift.
data.stateenumalwaysnot_started— No identity holds that identifier — show the sign-up form (POST /v1/auth/register, or POST /v1/auth/phone/otp/request).awaiting_phone_otp— A live register OTP exists but no identity yet — show the OTP screen with resend; continue at POST /v1/auth/registration/phone/verify.awaiting_password— The phone is proven but no password was chosen and no membership exists (scenario 3 step 3 pending) — “Finish setting up, choose a password”. Continuing costs a FRESH OTP first: the original registrationToken is short-lived by design and the flow may resume days later on another machine.awaiting_email_code— The email is not yet verified (scenario 2 step 2 pending) — show the code screen + resend. Reported even when the code has EXPIRED, with canResend: true: “your code lapsed, get another” is the right screen, where “nothing in flight” would strand the user.awaiting_invitation— An invited membership is waiting — continue at POST /v1/auth/invitations/accept.complete— Active account with an active membership — “your account is ready, log in”.
data.methodenumalwaysemail— The outstanding step is on the email channel.phone— The outstanding step is on the phone channel.
data.maskedDestinationstringconditionaldata.nextStepstringalwaysdata.canResendbooleanalwaysdata.expiresAttimestampconditional/v1/auth/registration/resumepublicResume by phone numberThe same endpoint addressed by phone — this is the call that recovers the awaiting_password state left by scenario 3 step 2, and the awaiting_phone_otp state left by a live register OTP with no identity yet.
Field guide — what each value means & where it comes from2 fields
Public, throttled per IP. Submit EXACTLY ONE of email / phone — the identifier the user started with is all they have to remember — and the resolver answers with the step to render. It never returns a credential: no token, no code, no password reset. Deliberately an enumeration oracle (the same posture register's 409 email_already_registered already takes); CAPTCHA/bot defence remains a production prerequisite for this surface.
emailbodystringconditionalyou choosephonebodystringconditionalyou choosePOST https://api.kerja.team/v1/auth/registration/resumeResponse guide — what comes back & what each value means6 fields
200 with the resolver's answer. It tells you WHERE YOU ARE — never a token, a code, or a password reset. The state is DERIVED from facts that already exist (users.status, password_hash, the *_verified_at columns, live tokens, memberships), never from a duplicate state table which would be a second truth and the first thing to drift.
data.stateenumalwaysnot_started— No identity holds that identifier — show the sign-up form (POST /v1/auth/register, or POST /v1/auth/phone/otp/request).awaiting_phone_otp— A live register OTP exists but no identity yet — show the OTP screen with resend; continue at POST /v1/auth/registration/phone/verify.awaiting_password— The phone is proven but no password was chosen and no membership exists (scenario 3 step 3 pending) — “Finish setting up, choose a password”. Continuing costs a FRESH OTP first: the original registrationToken is short-lived by design and the flow may resume days later on another machine.awaiting_email_code— The email is not yet verified (scenario 2 step 2 pending) — show the code screen + resend. Reported even when the code has EXPIRED, with canResend: true: “your code lapsed, get another” is the right screen, where “nothing in flight” would strand the user.awaiting_invitation— An invited membership is waiting — continue at POST /v1/auth/invitations/accept.complete— Active account with an active membership — “your account is ready, log in”.
data.methodenumalwaysemail— The outstanding step is on the email channel.phone— The outstanding step is on the phone channel.
data.maskedDestinationstringconditionaldata.nextStepstringalwaysdata.canResendbooleanalwaysdata.expiresAttimestampconditional