Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.prelude.so/llms.txt

Use this file to discover all available pages before exploring further.

Once a user is authenticated, you may want to let them attach a new identifier to their account: add a recovery phone number, switch to a new email address, enable a second login channel. Prelude exposes two reserved scopes that drive this flow end-to-end:
ScopeIdentifier addedOTP step
prld:phone:registerphone_numberverify_sms
prld:email:registeremail_addressverify_email
Register-identifier scopes are preformatted: Prelude runs the OTP challenge itself and does not call your delegation hook. You still need to declare the scope in your step-up configuration’s allowed_scopes with mode: "managed" so the frontend is permitted to request it. The client passes the identifier value on the request, Prelude validates it, runs the OTP challenge, and persists the identifier on the user’s profile when the challenge completes.

How it works

The new identifier is only attached to the user when the OTP step succeeds. If the user abandons the challenge or fails to enter a valid code, no change is made to their profile.

Prerequisites

  • A Prelude account with access to the Session API
  • An Application ID (appID) — see Applications
  • Step-up enabled on the application — see Step-Up Authentication. Register scopes reuse the step-up signing keys and JWKS, so step-up must be configured on the app
  • The register scope you want to expose declared in allowed_scopes on the step-up configuration with mode: "managed" (prld:phone:register and/or prld:email:register). The OTP step and the identifier write are handled by Prelude — no delegation hook is called for these entries
  • An OTP login configuration matching the identifier type you want to register (an SMS configuration for prld:phone:register, an email configuration for prld:email:register)
Register scopes must appear in allowed_scopes on the step-up configuration with mode: "managed". A request for a scope that is not allowed is rejected with scope_not_allowed before any OTP is sent. The managed mode is reserved for these two scopes and rejects any other scope at configuration time.
curl -X POST https://api.prelude.dev/v2/session/apps/${APP_ID}/config/stepup \
  -H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "step_keys": [],
    "allowed_scopes": [
      { "scope": "prld:phone:register", "mode": "managed" },
      { "scope": "prld:email:register", "mode": "managed" }
    ]
  }'

Trigger the flow

The frontend SDK initiates the flow with a regular requestStepUp call. The new identifier is passed on the request metadata under the key identifier:
// Add a phone number
await session.requestStepUp({
  scope: 'prld:phone:register',
  metadata: { identifier: '+15551234567' }
});

// Add an email address
await session.requestStepUp({
  scope: 'prld:email:register',
  metadata: { identifier: 'user@example.com' }
});
Prelude returns status=review with a single OTP step keyed verify_sms or verify_email. From there the SDK drives the OTP step exactly like any other step-up challenge — see the Web SDK Step-Up guide for the full code path.

What Prelude does

  1. Parse the value carried under metadata.identifier. Phone numbers are normalized to E.164; emails are lowercased and normalized.
  2. Reject malformed values with a bad_request error before any OTP is sent.
  3. Check uniqueness: if the identifier is already attached to a user (the requesting user or anyone else), the request is rejected with identifier_already_exists (HTTP 409). The OTP is never sent in this case.
  4. Synthesize a single-use challenge with one OTP step. The grant lifetime and the step expiration are both 10 minutes (granted_for: 600).
  5. Embed the canonical identifier on the challenge token so the value cannot be substituted by the client between stepup/request and the OTP completion.
  6. On OTP success, attach the identifier to the user via the same path as the Add Identifier management endpoint. A user.identifier.created webhook event is emitted.
If a competing flow registers the same identifier between stepup/request and the OTP completion, the OTP check fails with identifier_already_exists and the identifier is not attached.

Constraints

RuleLimit
metadata.identifierRequired. Phone numbers must be valid E.164. Email addresses must be syntactically valid. Maximum 320 characters (other step-up metadata values are capped at 32).
Grant lifetimeFixed at 10 minutes. The grant uses grant_mode: "single-use" and is consumed by the identifier write itself; the client should not reuse the resulting access token for anything else.
Identifier ownershipThe identifier must not already be attached to any user on the application. Use Delete Identifier first if you want to move an identifier between users.
OTP login configurationAn OTP configuration matching the identifier type must exist on the app. Without it, the OTP step cannot be sent.

Errors

StatusCodeCause
400bad_requestmetadata.identifier is missing, malformed, or exceeds the metadata value limit.
409identifier_already_existsThe value is already attached to a user — either at request time, or as a race when the OTP completes.
All other step-up errors (expired challenge, invalid token, replay, etc.) apply unchanged.

What’s next?

Step-Up Authentication

Full step-up overview, including custom scopes and the delegation hook.

Change Password

Companion preformatted flow for the prld:pwd:write scope.