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 · awaiting_tenant → login, which answers noOrganization (scenario 15) · complete → login
/v1/auth/registration/resumepublicWhere did I stop?Exactly one of { email } / { phone }. The seven states — not_started, awaiting_phone_otp, awaiting_password, awaiting_email_code, awaiting_invitation, awaiting_tenant (active, everything proven, NO membership of any kind — scenario 15; it used to report complete and send the user to a login that answered 403), complete — 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 { state, method, maskedDestination, nextStep, canResend, expiresAt } with the step to render. It never returns a credential: no token, no code, no password reset. The state is DERIVED from facts that already exist (status, password hash, *_verified_at, live tokens, memberships), with precedence: a finished account outranks a stale token, a pending invitation outranks a half-finished self sign-up, an active membership outranks an invited one. SEVEN states: not_started (no user with that identifier → the sign-up form) · awaiting_phone_otp (phone, no user, a live register OTP → the OTP screen with resend) · awaiting_password (user, phone verified, no password, no membership → “finish setting up — choose a password”; the phone is re-challenged before a registrationToken is re-issued) · awaiting_email_code (email unverified — reported even when the code has LAPSED, with canResend: true) · awaiting_invitation (a membership is invited → “you have a pending invitation”) · awaiting_tenant (user active, everything proven, NO membership of any kind → “create your organization”, §136 / scenario 15 — its nextStep is still POST /v1/auth/login, because resume cannot mint the bootstrapToken and login answers noOrganization with it; this used to report complete and send the user to a login that answered 403) · complete (active, ≥1 active membership → “log in”; also a user whose only membership is SUSPENDED — login then reports the real reason). 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.awaiting_tenant— Active account, everything proven, but NO membership of any kind — typically a member removed from their only organization (§136, scenario 15). Render “Create your organization”. nextStep is still POST /v1/auth/login, and deliberately: resume never hands back a credential, and login is what answers noOrganization with the bootstrapToken for POST /v1/auth/organizations/bootstrap. (This state used to report complete, which sent the user to a login that answered 403.)complete— Active account with at least one active membership — “your account is ready, log in”. Also reported for a user whose only membership is SUSPENDED; login then gives the real reason.
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 { state, method, maskedDestination, nextStep, canResend, expiresAt } with the step to render. It never returns a credential: no token, no code, no password reset. The state is DERIVED from facts that already exist (status, password hash, *_verified_at, live tokens, memberships), with precedence: a finished account outranks a stale token, a pending invitation outranks a half-finished self sign-up, an active membership outranks an invited one. SEVEN states: not_started (no user with that identifier → the sign-up form) · awaiting_phone_otp (phone, no user, a live register OTP → the OTP screen with resend) · awaiting_password (user, phone verified, no password, no membership → “finish setting up — choose a password”; the phone is re-challenged before a registrationToken is re-issued) · awaiting_email_code (email unverified — reported even when the code has LAPSED, with canResend: true) · awaiting_invitation (a membership is invited → “you have a pending invitation”) · awaiting_tenant (user active, everything proven, NO membership of any kind → “create your organization”, §136 / scenario 15 — its nextStep is still POST /v1/auth/login, because resume cannot mint the bootstrapToken and login answers noOrganization with it; this used to report complete and send the user to a login that answered 403) · complete (active, ≥1 active membership → “log in”; also a user whose only membership is SUSPENDED — login then reports the real reason). 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.awaiting_tenant— Active account, everything proven, but NO membership of any kind — typically a member removed from their only organization (§136, scenario 15). Render “Create your organization”. nextStep is still POST /v1/auth/login, and deliberately: resume never hands back a credential, and login is what answers noOrganization with the bootstrapToken for POST /v1/auth/organizations/bootstrap. (This state used to report complete, which sent the user to a login that answered 403.)complete— Active account with at least one active membership — “your account is ready, log in”. Also reported for a user whose only membership is SUSPENDED; login then gives the real reason.
data.methodenumalwaysemail— The outstanding step is on the email channel.phone— The outstanding step is on the phone channel.
data.maskedDestinationstringconditionaldata.nextStepstringalwaysdata.canResendbooleanalwaysdata.expiresAttimestampconditional