Send the primary phone OTP
Sends or resends the owner-authorization or new-number OTP for the caller's primary-phone-number change. The otp of the response tells where it went. A user token calls this on behalf of the signed-in user. Returns 404 when the phone number is not a contact on the caller's profile, and 409 when the current-account state changed and the flow must restart.
Authorization
oauth2 Create a client with POST /v1/oauth/clients, then exchange its credentials for an access token. AUTHORIZATION_CODE clients act on behalf of a signed-in user and always use PKCE; CLIENT_CREDENTIALS clients act as themselves. A client may only be granted scopes its creator already holds — read the grantable list from GET /v1/oauth/clients/scopes.
In: header
Request Body
application/json
The validated phone number to make the caller's primary phone number. When authorization_channel is omitted, the owner-authorization OTP goes to the caller's current primary contact of the target type.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/v1/user/settings/phones/primary/otp/send" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+254712345678" }'{ "status": "OTP_REQUIRED", "contact": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "value": "jane@example.com", "otp": { "step": "OWNER_AUTHORIZATION", "sent_to": "EMAIL", "steps": 2, "digits": 6 }, "is_primary": true, "validated": true, "validated_at": "2026-01-15T09:20:00Z", "requires_verification": true, "verification_locked": true, "verification_locked_at": "2026-01-20T16:45:00Z", "created_at": "2026-09-01T10:15:30Z" }, "otp": { "step": "OWNER_AUTHORIZATION", "sent_to": "EMAIL", "steps": 2, "digits": 6 }, "otp_url": "/v1/user/settings/emails/primary/otp/send", "confirm_url": "/v1/user/settings/emails/primary/otp/confirm", "status_url": "/v1/user/settings/emails/primary/change", "identity_sync_pending": true}{ "status": "OTP_REQUIRED", "contact": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "value": "jane@example.com", "otp": { "step": "OWNER_AUTHORIZATION", "sent_to": "EMAIL", "steps": 2, "digits": 6 }, "is_primary": true, "validated": true, "validated_at": "2026-01-15T09:20:00Z", "requires_verification": true, "verification_locked": true, "verification_locked_at": "2026-01-20T16:45:00Z", "created_at": "2026-09-01T10:15:30Z" }, "otp": { "step": "OWNER_AUTHORIZATION", "sent_to": "EMAIL", "steps": 2, "digits": 6 }, "otp_url": "/v1/user/settings/emails/primary/otp/send", "confirm_url": "/v1/user/settings/emails/primary/otp/confirm", "status_url": "/v1/user/settings/emails/primary/change", "identity_sync_pending": true}{ "code": "BAD_REQUEST", "description": "Bad Request"}{ "code": "UNAUTHORISED", "description": "Unauthorised"}{ "code": "FORBIDDEN", "description": "Forbidden"}{ "status": "OTP_REQUIRED", "contact": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "value": "jane@example.com", "otp": { "step": "OWNER_AUTHORIZATION", "sent_to": "EMAIL", "steps": 2, "digits": 6 }, "is_primary": true, "validated": true, "validated_at": "2026-01-15T09:20:00Z", "requires_verification": true, "verification_locked": true, "verification_locked_at": "2026-01-20T16:45:00Z", "created_at": "2026-09-01T10:15:30Z" }, "otp": { "step": "OWNER_AUTHORIZATION", "sent_to": "EMAIL", "steps": 2, "digits": 6 }, "otp_url": "/v1/user/settings/emails/primary/otp/send", "confirm_url": "/v1/user/settings/emails/primary/otp/confirm", "status_url": "/v1/user/settings/emails/primary/change", "identity_sync_pending": true}{ "status": "OTP_REQUIRED", "contact": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "value": "jane@example.com", "otp": { "step": "OWNER_AUTHORIZATION", "sent_to": "EMAIL", "steps": 2, "digits": 6 }, "is_primary": true, "validated": true, "validated_at": "2026-01-15T09:20:00Z", "requires_verification": true, "verification_locked": true, "verification_locked_at": "2026-01-20T16:45:00Z", "created_at": "2026-09-01T10:15:30Z" }, "otp": { "step": "OWNER_AUTHORIZATION", "sent_to": "EMAIL", "steps": 2, "digits": 6 }, "otp_url": "/v1/user/settings/emails/primary/otp/send", "confirm_url": "/v1/user/settings/emails/primary/otp/confirm", "status_url": "/v1/user/settings/emails/primary/change", "identity_sync_pending": true}{ "code": "RATE_LIMIT_EXCEEDED", "description": "Too Many Requests"}{ "code": "INTERNAL_ERROR", "description": "Internal Server Error"}Confirm the primary phone OTP POST
Confirms the OTP for the step named by `step`: `OWNER_AUTHORIZATION` or `NEW_CONTACT_VERIFICATION`. A user token calls this on behalf of the signed-in user. Returns 404 when the phone number is not a contact on the caller's profile, and 409 when the current-account state changed and the flow must restart.
Start the expiration sweep POST
Runs the stale-session expiry sweep for the caller’s organization. The service also runs this job on its own schedule across every organization; this endpoint is the operator-triggered form, narrowed to one tenant. The sweep reconciles applications that have sat in ID_VERIFICATION_PENDING past the provider submission window: a job the provider reports as final is completed, and one the provider no longer knows about is moved to EXPIRED — nothing is expired without asking the provider first. Authenticated with an internal user’s org-scoped bearer token and authorized by a WorkOS check for org:kyc.override:write on the caller’s own organization. The organization is resolved from the token; passing organization_id is rejected. The run is held to the same interval as the scheduled one — there is no force bypass — and each organization has its own lease and due-window, so a manual run neither blocks nor is blocked by the global schedule.