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

# Register an Identifier

> Add a phone number or email address to a logged-in user with an OTP challenge.

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:

| Scope                 | Identifier added | OTP step       |
| --------------------- | ---------------- | -------------- |
| `prld:phone:register` | `phone_number`   | `verify_sms`   |
| `prld:email:register` | `email_address`  | `verify_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

```mermaid theme={null}
sequenceDiagram
  autonumber
  actor U as User (logged in)
  participant SDK as Frontend SDK
  participant P as Prelude API

  U->>SDK: Submit new phone / email
  SDK->>P: POST /v1/session/stepup/request<br/>(scope=prld:phone:register, metadata.identifier=+1555…)
  P-->>P: Validate value, check uniqueness
  P-->>SDK: status=review, steps=[verify_sms]

  loop Managed OTP step
    U->>SDK: Enter OTP code
    SDK->>P: startOTP / checkOTP (challengeId)
    P-->>SDK: step done
  end

  P-->>P: Attach identifier to user
  P-->>SDK: step_up_token (challenge complete)
  P->>P: Emit user.identifier.created event
```

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](/session/documentation/applications)
* Step-up enabled on the application — see [Step-Up Authentication](/session/documentation/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`)

<Note>
  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.
</Note>

```bash theme={null}
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`:

```ts theme={null}
// 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](/session/documentation/frontend-sdks/web/step-up) 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](/session/api-reference/management/identifiers/create-identifier). 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

| Rule                    | Limit                                                                                                                                                                                                                       |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `metadata.identifier`   | Required. 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 lifetime          | Fixed 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 ownership    | The identifier must not already be attached to any user on the application. Use [Delete Identifier](/session/api-reference/management/identifiers/delete-identifier) first if you want to move an identifier between users. |
| OTP login configuration | An OTP configuration matching the identifier type must exist on the app. Without it, the OTP step cannot be sent.                                                                                                           |

## Errors

| Status | Code                        | Cause                                                                                                  |
| ------ | --------------------------- | ------------------------------------------------------------------------------------------------------ |
| 400    | `bad_request`               | `metadata.identifier` is missing, malformed, or exceeds the metadata value limit.                      |
| 409    | `identifier_already_exists` | The value is already attached to a user — either at request time, or as a race when the OTP completes. |

All other [step-up errors](/session/api-reference/frontend/stepup-request) (expired challenge, invalid token, replay, etc.) apply unchanged.

## What's next?

<CardGroup cols={2}>
  <Card title="Step-Up Authentication" icon="shield-check" href="/session/documentation/step-up-authentication">
    Full step-up overview, including custom scopes and the delegation hook.
  </Card>

  <Card title="Change Password" icon="key-skeleton" href="/session/documentation/change-password">
    Companion preformatted flow for the `prld:pwd:write` scope.
  </Card>
</CardGroup>
