> For the complete documentation index, see [llms.txt](/llms.txt)

# Biometric Passkey: Core API integration

## Introduction

This guide is for engineers integrating your backend with the Biometric Passkey Core API. It covers the prerequisites, supported integration flows and the API contract concepts shared by every flow, webhook delivery, error handling, observability, and versioning.

The Core API is the API your backend calls. The Management API is a separate operations API used by your operations teams and is covered in [Biometric Passkey: Dashboard and API management](/guide/biometric-passkey-management).

> ⚠️ **Warning:** **Please note:** The Biometric Passkey API is exclusively for the management of biometric passkey credentials and is distinct and separate from the Entrust IDV API. For integrating with the Entrust IDV API, please refer to our documentation [here](/sdk).

## Before you begin

Confirm the following before you start server-side integration:

- A Biometric Passkey backend is deployed in your infrastructure with TLS-terminating ingress that limits inbound access to your backend and the IDV webhook source. See [Biometric Passkey: Deployment](/guide/biometric-passkey-deployment).
- Confirm your platform team has deployed the Core API with the required runtime configuration before sending traffic to it. This guide names the integration-facing values that shape API behavior; the deployment guide covers release bundles, chart values, runtime Secrets, and infrastructure setup. See [Configuration values used by Core API flows](#configuration-values-used-by-core-api-flows).
- Your application has a registered passkey relying party (RP), owned by your backend or your IDP. Biometric Passkey does not act as the RP and does not perform WebAuthn cryptographic verification.
- Your backend has a stable per-user identifier that you will pass as `external_user_id`. The value is opaque to Biometric Passkey and must use URL-safe characters only: `A-Z`, `a-z`, `0-9`, `.`, `_`, `~`, and `-`. If your canonical user identifier uses other characters, map it to a stable URL-safe surrogate before calling the Core API.
- Your mobile applications have integrated the Biometric Passkey SDK. The SDK and any browser client talk to your backend, not to the Core API directly. See [Biometric Passkey: SDK integration](/guide/biometric-passkey-sdk).

## Integration architecture

For the component model, ownership split, and the diagram, see [Integration overview: Integration architecture](/guide/biometric-passkey-integration-overview#integration-architecture).

## API basics

### Request and response conventions

| Item | Details |
| --- | --- |
| Transport | All request and response bodies are JSON. Timestamps are RFC 3339 UTC strings. |
| Identifiers | Your application supplies a stable URL-safe per-user identifier (`external_user_id`); the Core API mints a per-flow session identifier when a new flow is opened. Treat all identifiers as opaque strings. |
| Opaque tokens | The Core API issues short-lived tokens to bind specific transitions inside a flow (`finalize_token`, `handoff_token`, `continuation_token`, `recovery_token`). Treat returned tokens as opaque; do not parse, log, or persist them beyond the in-flight flow. Lifetimes are listed under [Token lifetimes](#token-lifetimes). |
| State machines | Each flow advances by explicit calls; sessions expire automatically per TTL. |

**Request headers**

- `X-Correlation-ID` (optional, recommended) — an opaque identifier you assign to each end-user action. Biometric Passkey records it on backend logs and audit events so a single failure can be traced across your edge, your backend, and the management dashboard.
- `Idempotency-Key` (optional) — an opaque caller-chosen identifier accepted only by the flow-opening (start) endpoints. On those endpoints, Biometric Passkey combines the key with the request's business context to recognise a retry and return the existing in-flight attempt or session instead of opening a duplicate. Other endpoints replay through the path-bound session identifier or the flow state machine.

### Configuration values used by Core API flows

The Core API request and response contract is independent of the deployment mechanism. Before you call the API, confirm these integration-facing values with the team that operates your Biometric Passkey backend:

| Area | Value to confirm | Integration impact |
| --- | --- | --- |
| Core API origin | HTTPS origin for your deployed Core API | Your backend sends all `/api/internal/v1/...` requests to this origin. Browser and mobile clients do not call the Core API directly. |
| Registration workflow ID | Published Entrust Identity Verification workflow used for enrollment | Registration start requests use this workflow to perform identity verification and produce the encrypted biometric token stored for later checks. |
| Step-up workflow ID | Published Entrust Identity Verification workflow used for high-assurance authentication | Step-up start requests use this workflow for live biometric verification against the stored encrypted biometric token. |
| Recovery workflow ID | Published Entrust Identity Verification workflow used during account recovery | Recovery requests use this workflow to verify the recovering user before replacement credential creation. |
| IDV webhook route | Provider callback route for identity verification results | Flow status can remain pending until webhook delivery succeeds. Your backend does not call this route; it polls the flow status endpoints documented below. |

For the deployment-time configuration and infrastructure settings behind these values, see [Biometric Passkey: Deployment](/guide/biometric-passkey-deployment).

### Errors and retries

All errors return the same envelope:

```json
{
  "error": {
    "code": "string",
    "message": "string",
    "retryable": false,
    "details": { "field": "string" }
  }
}
```

`error.details` is optional. When present, it is a flat map of string values. Use it to identify the offending field name or a short machine-readable hint to your operators; do not rely on nested structures.

| Status | Typical causes | Recommended action |
| --- | --- | --- |
| 400 validation | Missing field, malformed payload, invalid request body, or schema validation failure | Fix the client; do not retry the same request unchanged. |
| 403 forbidden | Caller ownership, auth context, or recovery-token context is not authorized for the requested resource | Check the user and flow binding; do not blindly retry. |
| 404 not found | The referenced session or user does not exist | Verify the identifier; do not retry. |
| 408 timeout | The request timed out or was canceled before completion | Retry the same logical request. |
| 409 conflict | Token already consumed, state transition not legal, expired session, or retryable wait state such as `WORKFLOW_NOT_COMPLETED` or `RESOURCE_BUSY` | Branch on `error.code` and `error.retryable`. Retry only when `retryable` is `true`; otherwise inspect state or restart the affected flow. |
| 422 unprocessable | Payload is well-formed but fails flow-specific semantic validation, such as an invalid passkey bundle, applicant input, workflow input, or prior-biometric input | Fix the supplied data or issue a fresh passkey bundle; do not retry the same payload unchanged. |
| 429 rate limited | Excessive request rate on recovery start or recovery IDV start (`RECOVERY_RATE_LIMITED`). Apply any broader request-rate policy at your ingress. | Honor `Retry-After`; back off and retry. |
| 500 internal | Unhandled server-side error inside the Core API, including temporary webhook processing delay | Retry with exponential backoff, preserving `Idempotency-Key` when the original request carried one. |
| 502 upstream | The identity verification provider is unavailable or returned an unexpected response | Retry with exponential backoff. Persistent `502` is an operational incident, not a client bug. |

The `error.retryable` boolean is the authoritative signal for safe retry. When `retryable` is `true`, retry with the same payload. Preserve the same `Idempotency-Key` only on endpoints that accept the header; path-bound session endpoints replay through the session identifier and flow state machine.

Two flow-specific cases are worth handling explicitly:

- **Enrollment and recovery conflicts:** a user can have at most one active enrollment attempt and at most one active recovery attempt at the same time, and recovery start is blocked while enrollment is in flight for the same user. Duplicate enrollment starts are rejected unless you replay the original start with the same `Idempotency-Key`. Recovery start does not accept `Idempotency-Key`; if an active recovery attempt already exists for the same user, Biometric Passkey returns `200 OK` with that attempt and a fresh replay of the same `recovery_token`. If recovery is blocked by an active enrollment attempt, the backend returns HTTP 409 with `error.code = "REGISTRATION_ATTEMPT_CONFLICT"`. Step-up authentication, both in-app and cross-platform, is not affected because sessions are scoped per request context, not per user.
- **Reserved identity verification workflow input:** the `encrypted_biometric_token` key inside `workflow_input.custom_data` is reserved by the Biometric Passkey backend, which supplies that workflow input on your behalf from either the `prior_biometric` field at registration time or the user's stored biometric reference for step-up and recovery. If your request sets `workflow_input.custom_data.encrypted_biometric_token`, the backend rejects it with HTTP 422 and `error.code = "IDV_WORKFLOW_INPUT_INVALID"`. Remove the key from your request payload and let the backend populate it.

### Webhook delivery

Identity verification runs asynchronously. The identity verification provider delivers result events to your Biometric Passkey backend, which validates and processes them to advance flow status. Your backend does not implement or call this webhook; it is reached only by the provider.

What this means for your integration:

- You do not handle the webhook. When a result arrives, the affected flow advances on its own to its identity verification completed or failed state.
- Observe results by polling the per-flow status endpoint listed under [Integration walkthrough](#integration-walkthrough). Polling is safe and idempotent; preserve `X-Correlation-ID` across polls to keep traces joinable.
- Flow status can remain pending until a result is delivered, so treat status transitions as eventual rather than immediate.

Webhook signature verification and the provider-event freshness window are configured at deployment time. See [Biometric Passkey: Deployment](/guide/biometric-passkey-deployment).

### Logging, correlation, and observability

For every Core API request your backend issues:

- Send a single `X-Correlation-ID` per logical end-user action. Reuse it across all backend requests, polling calls, and webhook handlers that belong to the same flow.
- Log the request method, path, response status, `error.code` (if any), and the correlation identifier. Do not log any field named `token` or ending in `_token`, SDK IDV tokens, EBT ciphertext, WebAuthn payloads, or signatures.
- Preserve the correlation identifier through your edge logs so a single user-visible failure can be traced end-to-end from your edge to the Core API to the identity verification provider and to the management dashboard.

### Versioning and deprecation

The Core API uses a versioned path prefix that is decoupled from the deployment release cadence.

- The backend integration API is exposed under the path prefix `/api/internal/v1`. Every endpoint documented in [the endpoint groups below](#registration-endpoints) lives under this prefix. In this prefix, `internal` means browser and mobile clients do not call these endpoints directly; your backend is the caller.
- Treat the hosted OpenAPI reference for your deployed release as the contract source. Your client must tolerate unknown response fields and unknown error codes by falling back to `error.retryable`.
- Pin your client to a single path prefix per environment. Confirm the supported prefix and any migration guidance for your release with your Entrust contact.
- `GET /api/health` is the liveness and health endpoint. `GET /api/version` returns the running server's build information for identifying which version a deployment is serving.

For supported backend release lines, SDK compatibility, upgrade planning, and the compatibility matrix, see [Biometric Passkey: Version policy](/guide/biometric-passkey-version-policy).

## Integration walkthrough

This section covers the four flows your backend integrates against the Core API: registration, in-app step-up, cross-platform step-up, and account recovery. Each subsection has a sequence diagram, the endpoints your backend calls, the steps in order, and the failure paths to handle.

### How every flow is shaped

All four flows follow the same pattern, so it is worth reading this once before the individual flows:

- **Your backend is the only caller of the Core API.** Your app and the SDK talk to your backend; they never call the Core API directly. Your backend forwards challenges and tokens out to the device and forwards the device's results back to the Core API.
- **Your IDP owns the passkey; the Core API binds identity to it.** The Core API is not a WebAuthn relying party and performs no WebAuthn cryptographic verification. Your IDP issues the challenge and verifies the attestation or assertion. The Core API ties an identity verification result to that passkey operation.
- **Identity verification is asynchronous.** The SDK captures identity on the device against Entrust Identity Verification. The provider returns the result to the Core API separately, and the flow advances on its own. Your backend observes progress by polling the flow's status endpoint; it never handles the provider callback.
- **Completion is a prepare → commit → finalize handshake.** `prepare-complete` binds the device's attestation or assertion and returns a short-lived `finalize_token`. Your backend commits on your IDP, then calls `finalize` (or `abort`). These last calls usually run inside a single backend operation.

Every identifier and token below is opaque. Never parse, log in plaintext, or persist a token beyond the in-flight flow. Reuse `external_user_id` (your stable per-user identifier) across all flows for a given user; treat every Core-minted identifier (`registration_attempt_id`, `km_passkey_session_id`, `cross_platform_session_id`, `recovery_attempt_id`) as a per-flow handle.

### Token lifetimes

| Token | Lifetime | Single-use | Notes |
| --- | --- | --- | --- |
| `finalize_token` | 5 minutes | Yes | Returned by `prepare-complete`; consumed by `finalize` or `abort`. |
| `handoff_token` | 10 minutes | Yes | Returned by cross-platform `start`; consumed by `resume`. Omitted on a start replay, so persist it when first issued. |
| `recovery_token` | 10 minutes (recovery attempt TTL) | No | Reusable within the attempt; survives the continuation exchange. |
| `continuation_token` | 10 minutes (recovery attempt TTL) | No | Reusable across recovery `registration/start` and `prepare-complete` within the attempt. |

Sessions and attempts expire on their own TTL: a cross-platform session lives 15 minutes and a recovery attempt lives 10 minutes. Registration and step-up attempts expire at the `expires_at` your IDP sets on the passkey bundle.

### Registration

Enroll a user: verify their identity with a document and face capture, create a passkey, and bind the two together. Registration runs on a mobile device because it needs document and biometric capture.

![Registration flow: backend orchestrates IDV verification and credential creation, then finalizes to bind the passkey to the verified identity.](./biometric-passkey-api-integration-flow-registration.png)

#### Steps

1. **Open the attempt.** Ask your IDP for a passkey registration challenge, then call `POST /registrations/start` with the user's `external_user_id`, the `applicant` fields your identity verification workflow requires (at a minimum `first_name` and `last_name`), and the IDP-issued `passkey_registration` bundle (`challenge`, `user_handle`, `rp_id`, `expires_at`). The Core API opens the attempt and returns `registration_attempt_id` and an IDV token. Forward the IDV token and the IDP's `publicKeyCreationOptions` to your app. Send an `Idempotency-Key` so a network retry returns the same attempt instead of opening a duplicate. To re-enroll a user whose encrypted biometric token your backend already holds, also set `prior_biometric.encrypted_biometric_token` on this request; never set `workflow_input.custom_data.encrypted_biometric_token`, which the Core API reserves.
2. **Verify identity and create the passkey on the device.** The SDK consumes the IDV token, captures the document and face against Entrust Identity Verification, and runs the WebAuthn ceremony locally. When the workflow completes, the provider notifies the Core API, which stores the user's biometric reference and advances the attempt to `idv_completed`. The SDK returns the `attestation_object` and `client_data_json` to your backend. You may poll `GET /registrations/{id}`, but you do not have to — the next step reports a clear error if the result has not arrived yet.
3. **Bind the credential.** Call `POST /registrations/{id}/prepare-complete` with the attestation. The Core API extracts and stores the WebAuthn `credential_id`, binds it to the attempt, and returns a `finalize_token` valid for 5 minutes.
4. **Commit and finalize.** Commit the passkey on your IDP, then call `POST /registrations/{id}/finalize` with the `finalize_token` to activate the credential. On IDP commit failure, call `POST /registrations/{id}/abort` with the same `finalize_token` and an `error_code`; the attempt ends in `idp_commit_failed`. To close an attempt the user backs out of before `prepare-complete`, call `POST /registrations/{id}/cancel`.

The attempt advances `idv_in_progress` → `idv_completed` → `idp_commit_pending` → `completed`. Failure surfaces as `idp_commit_failed`, `failed`, `cancelled`, or `expired`.

#### Endpoints

| Method | Endpoint | When your backend calls it |
| --- | --- | --- |
| POST | [Start a registration attempt](#start-a-registration-attempt) | Always, once per attempt |
| GET | [Get registration status](#get-registration-status) | Optional, to poll progress |
| POST | [Prepare registration completion](#prepare-registration-completion) | Always, after the SDK returns the attestation |
| POST | [Finalize registration](#finalize-registration) | On IDP commit success |
| POST | [Abort prepared registration](#abort-prepared-registration) | On IDP commit failure after `prepare-complete` |
| POST | [Cancel registration](#cancel-registration) | Optional, when the user backs out before `prepare-complete` |

#### Failure handling

| Trigger | What to do | Resulting state |
| --- | --- | --- |
| User backs out before `prepare-complete` | [Cancel registration](#cancel-registration) | `cancelled` |
| Identity verification fails or returns an unapproved result | Nothing; the attempt fails on its own | `failed` with an `error_code` reflecting the outcome |
| `finalize` returns `ENCRYPTED_BIOMETRIC_TOKEN_MISSING` (409) | Do not retry the token. Reconcile identity verification delivery and start a new attempt if enrollment should continue. | `failed`; the prepared credential is marked `registration_failed` |
| IDP commit fails after `prepare-complete` | [Abort prepared registration](#abort-prepared-registration) | `idp_commit_failed` |
| `finalize_token` reused or stale | Do not retry blindly; fetch status and reconcile | `FINALIZE_TOKEN_INVALID` (409) |
| Attempt TTL elapses | Nothing | `expired` |

### In-app step-up

Re-verify an already-enrolled user for a high-assurance action inside your mobile app: a live face capture against the user's stored biometric reference, plus a fresh WebAuthn assertion.

![In-app step-up flow: backend starts a step-up session, the app signs the challenge, and the backend finalizes to obtain the authentication assertion.](./biometric-passkey-api-integration-flow-in-app-step-up.png)

#### Steps

1. **Open the session.** Ask your IDP for a passkey authentication challenge, then call `POST /step-up/auth-sessions/start` with the user's `external_user_id` and the `passkey_authentication` bundle (`challenge`, `rp_id`, `expires_at`). The Core API returns `km_passkey_session_id`. The user must already have an enrolled credential; if not, start returns `404 PASSKEY_NOT_FOUND` and you must route them through registration. Forward the session id and the IDP's `publicKeyRequestOptions` to your app.
2. **Start identity verification.** The SDK computes a `credential_id_hint` (the credential it will use) and a `client_data_hash` (over the exact client data it will later sign) and returns both to your backend. Call `POST /step-up/auth-sessions/{id}/idv/start` with those two values. The Core API supplies the user's stored biometric reference to the workflow and returns an IDV token for the SDK. If the user has no usable biometric reference, this returns `ENCRYPTED_BIOMETRIC_TOKEN_MISSING` (409) and you must route them to recovery.
3. **Capture and sign on the device.** The SDK runs the live face capture against Entrust Identity Verification and the WebAuthn ceremony, then returns the assertion (`client_data_json`, `authenticator_data`, `signature`) to your backend.
4. **Bind, verify, and finalize.** Call `POST /step-up/auth-sessions/{id}/prepare-complete` with the assertion to get a `finalize_token` (valid 5 minutes). Verify the assertion on your IDP, then call `POST /step-up/auth-sessions/{id}/finalize` to mark the step-up `completed`. On IDP failure, call `POST /step-up/auth-sessions/{id}/abort` with the same `finalize_token` and an `error_code`; the session ends in `failed`.

The session advances `created` → `idv_in_progress` → `idv_completed` → `auth_finalizing` → `completed`. Failure surfaces as `failed`, `cancelled`, or `expired`.

#### Endpoints

| Method | Endpoint | When your backend calls it |
| --- | --- | --- |
| POST | [Start a step-up auth session](#start-a-step-up-auth-session) | Always, once per session |
| POST | [Start in-app step-up IDV](#start-in-app-step-up-idv) | Always, to obtain the IDV token |
| GET | [Get step-up auth session status](#get-step-up-auth-session-status) | Optional, to poll progress |
| POST | [Prepare step-up completion](#prepare-step-up-completion) | Always, after the SDK returns the assertion |
| POST | [Finalize step-up authentication](#finalize-step-up-authentication) | On IDP verification success |
| POST | [Abort prepared step-up authentication](#abort-prepared-step-up-authentication) | On IDP verification failure after `prepare-complete` |

#### Failure handling

| Trigger | What to do | Resulting state |
| --- | --- | --- |
| User has no usable biometric reference | Route the user to recovery | `idv/start` returns `ENCRYPTED_BIOMETRIC_TOKEN_MISSING` (409) |
| Identity verification fails | Nothing; the session fails on its own | `failed` |
| Credential hint malformed or unknown | Do not retry the same values; recompute from the actual SDK output | `idv/start` returns `INVALID_INPUT` (400) or `PASSKEY_NOT_FOUND` (404) |
| Client-data hash does not match the later assertion | Restart with a fresh challenge | `prepare-complete` returns `CLIENT_DATA_CONTINUITY_MISMATCH` (409) |
| IDP assertion verification fails | [Abort prepared step-up authentication](#abort-prepared-step-up-authentication) | `failed` |
| `finalize_token` reused or stale | Do not retry blindly; fetch status and reconcile | `FINALIZE_TOKEN_INVALID` (409) |
| Session TTL elapses | Nothing | `expired` |

### Cross-platform step-up

Step up a high-assurance action that starts in a browser or desktop session where there is no camera. Your backend hands the action off to the user's enrolled mobile device, which performs the same step-up as above; the browser learns the outcome by polling.

![Cross-platform step-up flow: web requests a handoff, the mobile app resumes the session, and the web side polls until completion.](./biometric-passkey-api-integration-flow-cross-platform-step-up.png)

#### Steps

1. **Open the session and deliver the handoff token.** From the browser side, call `POST /cross-platform-sessions/start` with the user's `external_user_id` and a `web_context` (`session_id` required, optional `actor_context_id`) that binds the session to the originating browser session. The Core API returns `cross_platform_session_id` and a single-use `handoff_token` (valid 10 minutes). Persist the handoff token when first issued — a start replay from the same browser context returns the existing session but omits the token. Deliver the token to the user's enrolled mobile device through a channel of your choice (push, QR, or deep link).
2. **Poll status from the browser.** While the user picks up their phone, the browser polls your backend, which calls `POST /cross-platform-sessions/{id}/status` with the same `external_user_id` and `web_context` the session was created with. The Core API re-validates ownership on every poll and collapses all in-progress states into `pending`, surfacing only the terminal states `completed`, `failed`, `expired`, and `cancelled`. Both session expiry and handoff-token expiry surface as `expired`.
3. **Resume on mobile.** When the user opens the handoff on the device, the mobile app returns the `handoff_token` to your backend. Call `POST /cross-platform-sessions/resume` with the token. The Core API consumes it (single-use), moves the session to `mobile_resumed`, and returns the continuation context. The rest of the flow runs on the mobile device.
4. **Run the mobile step-up.** Ask your IDP for an authentication challenge, then call `POST /cross-platform-sessions/{id}/auth-sessions/start` with the `passkey_authentication` bundle. This call takes no `external_user_id` or `web_context` — the Core API derives the owner from the session — and returns a `km_passkey_session_id`. The SDK computes `credential_id_hint` and `client_data_hash`; call `POST /cross-platform-sessions/{id}/idv/start` with `km_passkey_session_id`, `credential_id_hint`, and `client_data_hash` to get the IDV token. The SDK runs the live face capture and the WebAuthn ceremony and returns the assertion. On these cross-platform endpoints, `km_passkey_session_id` travels in the request body, not the path.
5. **Bind, verify, and finalize.** Call `POST /cross-platform-sessions/{id}/prepare-complete` with the assertion to get a `finalize_token`. Verify the assertion on your IDP, then call `POST /cross-platform-sessions/{id}/finalize`; on IDP failure call `POST /cross-platform-sessions/{id}/abort` with the same `finalize_token` and an `error_code`. The browser's next status poll then surfaces the terminal state. To close a session the user backs out of before `prepare-complete`, call `POST /cross-platform-sessions/{id}/cancel`.

The session advances `created` → `handoff_issued` → `mobile_resumed` → `idv_in_progress` → `idv_completed` → `auth_finalizing` → `completed`. Failure surfaces as `failed`, `expired`, or `cancelled`.

#### Endpoints

| Method | Endpoint | When your backend calls it |
| --- | --- | --- |
| POST | [Start a cross-platform session](#start-a-cross-platform-session) | Always, once per session |
| POST | [Get cross-platform session status](#get-cross-platform-session-status) | Always, while the browser awaits completion |
| POST | [Resume cross-platform session on mobile](#resume-cross-platform-session-on-mobile) | Always, once, on mobile pickup |
| POST | [Start cross-platform auth session](#start-cross-platform-auth-session) | Always, after `resume` |
| POST | [Start cross-platform IDV](#start-cross-platform-idv) | Always, after `auth-sessions/start` |
| POST | [Prepare cross-platform completion](#prepare-cross-platform-completion) | Always, after the SDK returns the assertion |
| POST | [Finalize cross-platform authentication](#finalize-cross-platform-authentication) | On IDP verification success |
| POST | [Abort prepared cross-platform authentication](#abort-prepared-cross-platform-authentication) | On IDP verification failure after `prepare-complete` |
| POST | [Cancel cross-platform session](#cancel-cross-platform-session) | Optional, when the user backs out before `prepare-complete` |

#### Failure handling

| Trigger | What to do | Resulting state |
| --- | --- | --- |
| User does not pick up the handoff before TTL | Nothing | Session and handoff token both `expired`; the poll surfaces `expired` |
| Handoff token already consumed (replay) | Nothing; the original resume stays in effect | `resume` returns a conflict |
| User has no usable biometric reference | Route the user to recovery | The mobile leg cannot continue; branch on the returned `error.code` |
| User rejects the action on the device before `prepare-complete` | [Cancel cross-platform session](#cancel-cross-platform-session) | `cancelled` |
| IDP assertion verification fails | [Abort prepared cross-platform authentication](#abort-prepared-cross-platform-authentication) | `failed` |
| `finalize_token` reused or stale | Do not retry blindly; fetch status and reconcile | `FINALIZE_TOKEN_INVALID` (409) |

### Account recovery

Restore access for a user who lost their device: re-verify them with a live face capture against their stored biometric reference, then enroll a replacement passkey. Recovery starts from your own lower-assurance recovery channel.

![Account recovery flow: backend orchestrates IDV verification, then re-runs registration to mint a replacement credential.](./biometric-passkey-api-integration-flow-recovery.png)

#### Steps

1. **Open the attempt.** Identify the recovering user through your own recovery channel first; do not call the Core API until you have one authoritative `external_user_id`. Call `POST /recovery-attempts/start` with that `external_user_id`. The Core API checks the user exists, has an active credential, and has a usable biometric reference, then returns `recovery_attempt_id` and a `recovery_token`. If the user or active credential is missing it returns `404 PASSKEY_NOT_FOUND`; if there is no usable biometric reference it returns `RECOVERY_EBT_UNAVAILABLE` (409). In either case, route the user to fresh enrollment or operator remediation. The Core API selects the stored biometric reference internally — you do not supply a credential or biometric selector.
2. **Verify identity.** Call `POST /recovery-attempts/{id}/idv/start` with the `recovery_token` to get the IDV token. (Recovery takes the `recovery_token` here instead of a credential hint and client-data hash, because there is no assertion to bind yet.) The SDK runs a live face capture against Entrust Identity Verification; on success the attempt moves to `idv_verified`. Your backend never sees the biometric reference in plaintext.
3. **Exchange the continuation token and open the replacement registration.** Once the attempt reaches `idv_verified`, call `POST /recovery-attempts/{id}/continuation/exchange` with the `recovery_token`. The Core API echoes the authoritative `external_user_id` and issues a `continuation_token`; the exchange does not invalidate the `recovery_token`, and a replay returns the existing `continuation_token`. Ask your IDP for a replacement registration challenge, then call `POST /recovery-attempts/{id}/registration/start` with the `continuation_token` and the `passkey_registration` bundle. The attempt moves to `registration_in_progress` and the SDK creates the new passkey on the device.
4. **Bind, commit, and finalize, with optional retry.** Call `POST /recovery-attempts/{id}/prepare-complete` with the `continuation_token` and the attestation to get a `finalize_token`. Commit the replacement on your IDP, then call `POST /recovery-attempts/{id}/finalize`. On IDP failure, call `POST /recovery-attempts/{id}/abort` with an `error_code` (and the `finalize_token` if one was issued); the attempt rewinds to `idv_verified`, so you can call `registration/start` again with the same `continuation_token` to retry without repeating the face capture. After `finalize` succeeds, use the [Management](/guide/biometric-passkey-management) endpoints to revoke the user's prior credentials if your policy requires it.

The attempt advances `created` → `idv_in_progress` → `idv_verified` → `registration_in_progress` → `idp_commit_pending` → `completed`. Failure surfaces as `failed`, `cancelled`, or `expired`. Unlike the other flows, recovery `abort` is a non-terminal rewind, not a terminal state.

#### Endpoints

| Method | Endpoint | When your backend calls it |
| --- | --- | --- |
| POST | [Start a recovery attempt](#start-a-recovery-attempt) | Always, once per attempt |
| GET | [Get recovery attempt status](#get-recovery-attempt-status) | Optional, to poll progress |
| POST | [Start recovery IDV](#start-recovery-idv) | Always, to obtain the IDV token |
| POST | [Exchange recovery continuation](#exchange-recovery-continuation) | Always, after IDV reaches `idv_verified` |
| POST | [Start recovery registration](#start-recovery-registration) | Always, with `continuation_token` |
| POST | [Prepare recovery completion](#prepare-recovery-completion) | Always, after the SDK returns the attestation |
| POST | [Finalize recovery](#finalize-recovery) | On IDP commit success |
| POST | [Abort prepared recovery registration](#abort-prepared-recovery-registration) | On IDP commit failure after `prepare-complete` |
| POST | [Cancel recovery attempt](#cancel-recovery-attempt) | Optional, when the user backs out before `prepare-complete` |

#### Failure handling

| Trigger | What to do | Resulting state |
| --- | --- | --- |
| User or active credential is missing | Route the user to fresh enrollment | `start` returns `PASSKEY_NOT_FOUND` (404) |
| Active credential exists but no usable biometric reference | Route the user to fresh enrollment or remediation | `start` returns `RECOVERY_EBT_UNAVAILABLE` (409) |
| Recovery identity verification fails | Nothing; the attempt fails on its own | `failed` |
| Invalid, unknown, or expired recovery token | Do not retry; fetch status and reconcile | `RECOVERY_TOKEN_INVALID` (403) |
| Invalid, unknown, or expired continuation token | Do not retry; fetch status and reconcile | `CONTINUATION_TOKEN_INVALID` (409) |
| IDP commit fails after `prepare-complete` | [Abort prepared recovery registration](#abort-prepared-recovery-registration) | Rewinds to `idv_verified`; retry with the same `continuation_token` |
| `finalize_token` reused or stale | Do not retry blindly; fetch status and reconcile | `FINALIZE_TOKEN_INVALID` (409) |
| User backs out before `prepare-complete` | [Cancel recovery attempt](#cancel-recovery-attempt) | `cancelled` |
| Attempt TTL elapses | Nothing | `expired` |

## Registration endpoints

These endpoints cover the initial enrollment flow: starting a registration attempt, polling status, preparing the IDP-side passkey completion, finalizing (or aborting) the prepared completion, and cancelling an in-flight attempt.

  ### Start a registration attempt

  `POST /api/internal/v1/registrations/start`

  Starts a new enrollment attempt for a user. Your backend supplies the IDP-issued passkey registration challenge bundle and the user's applicant profile. Biometric Passkey records the attempt, starts the identity verification session against Entrust Identity Verification, and returns a `registration_attempt_id` your backend uses for the rest of the flow.

  Use this endpoint when your application has decided to enroll a new user (or to add a new device-bound credential for an existing user) and your backend or IDP has already generated the passkey registration challenge.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | Idempotency-Key | Recommended | Required for safe retry. Reuse the same key when retrying the same logical attempt. |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | external_user_id | string | yes | Your stable per-user identifier. Opaque to Biometric Passkey. |
  | applicant | object | yes | Applicant profile fields (`first_name`, `last_name`, optional `dob`, `email`, `phone_number`, `address`) forwarded to Entrust Identity Verification. |
  | passkey_registration | object | yes | IDP-issued passkey registration challenge bundle: `challenge`, `user_handle`, `rp_id`, `expires_at`. |
  | workflow_input | object | optional | Forwarded to the Entrust Identity Verification workflow run. May carry `custom_data` and `tags`. |
  | device | object | optional | Mobile device metadata (`platform`, `device_label`). |
  | prior_biometric | object | optional | Pointer to a prior biometric reference, when re-enrolling an existing user. |

  #### Response

  Returns `201 Created` on a new attempt; returns `200 OK` when an active attempt can replay the original IDV start bundle for the same retry context.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | Opaque attempt identifier. Use in all subsequent calls in this flow. |
  | status | enum | One of `created`, `idv_in_progress`, `idv_completed`, `idp_commit_pending`, `idp_commit_failed`, `completed`, `failed`, `cancelled`, `expired`, `cleaned_up`. |
  | idv | object | `token` and `workflow_run_id` for the in-flight Entrust Identity Verification workflow run. The SDK consumes `token` to drive the on-device capture. |
  | expires_at | string (date-time) | Attempt TTL. After this time the attempt transitions to `expired` automatically. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 409 | A conflicting active attempt exists, or an active attempt cannot replay its IDV start bundle for the same retry context. |
  | 422 | The `passkey_registration` bundle is inconsistent (for example, `expires_at` is in the past). |
  | 502 | Entrust Identity Verification was unreachable; safe to retry with the same `Idempotency-Key`. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Get registration status

  `GET /api/internal/v1/registrations/{registration_attempt_id}`

  Returns the current state of a registration attempt. Use this endpoint to poll attempt progress while the on-device identity verification is running, to confirm a terminal state after the SDK reports completion, or to read the bound `credential_id` after the attempt finishes.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | The attempt identifier returned by [Start a registration attempt](#start-a-registration-attempt). |

  #### Response

  Returns `200 OK` with the current attempt snapshot.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | Echoes the path parameter. |
  | external_user_id | string | The user this attempt belongs to. |
  | status | enum | One of `created`, `idv_in_progress`, `idv_completed`, `idp_commit_pending`, `idp_commit_failed`, `completed`, `failed`, `cancelled`, `expired`, `cleaned_up`. |
  | workflow_run_id | string \| null | Entrust Identity Verification workflow run identifier, when started. |
  | credential_id | string \| null | WebAuthn/passkey credential bound at completion. Does not identify or scope the user's EBT. |
  | error_code | string \| null | Set on terminal failure states. |
  | expires_at | string (date-time) | Attempt TTL. |
  | updated_at | string (date-time) | Last modification timestamp on the attempt. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 404 | No registration attempt with this `registration_attempt_id` exists. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Prepare registration completion

  `POST /api/internal/v1/registrations/{registration_attempt_id}/prepare-complete`

  Submits the WebAuthn attestation captured on device. Biometric Passkey records the attestation against the attempt, transitions the attempt to `idp_commit_pending`, and returns a one-time `finalize_token` that the matching [finalize](#finalize-registration) or [abort](#abort-prepared-registration) request must consume.

  Call this endpoint once your mobile app has returned the authenticator attestation and your backend is ready to prepare the IDP commit. If the same prepare-complete request is retried while its token remains valid, Biometric Passkey returns the existing `finalize_token` when the WebAuthn binding is unchanged. Complete the attempt by calling finalize on IDP success or abort on IDP failure.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | The attempt identifier returned by [Start a registration attempt](#start-a-registration-attempt). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | attestation_object | string | yes | The raw attestation object from the authenticator (unpadded base64url). |
  | client_data_json | string | yes | Unpadded base64url encoding of the raw clientDataJSON bytes. |

  #### Response

  Returns `200 OK` with the issued `finalize_token` and the updated attempt status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `idp_commit_pending` after a successful prepare-complete. |
  | finalize_token | string | Opaque one-time token consumed by the matching finalize or abort request. |
  | expires_at | string (date-time) | Finalize-token expiry. The `finalize_token` is invalidated at this time. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 404 | No registration attempt with this `registration_attempt_id` exists. |
  | 409 | The attempt is not in a state that accepts a prepare-complete (for example, it is already finalized, aborted, or expired). |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Finalize registration

  `POST /api/internal/v1/registrations/{registration_attempt_id}/finalize`

  Completes the Biometric Passkey side of a prepared registration once your IDP has accepted the passkey registration. The first successful finalize consumes the `finalize_token` issued by [prepare-complete](#prepare-registration-completion) and transitions the attempt to `completed`.

  Call this endpoint after your IDP has committed the credential on its side. If the IDP commit fails instead, call [abort](#abort-prepared-registration) with the same `finalize_token`. Retrying after the attempt is terminal returns the current terminal snapshot.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | The attempt identifier returned by [Start a registration attempt](#start-a-registration-attempt). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | finalize_token | string | yes | The one-time token returned by [prepare-complete](#prepare-registration-completion). |

  #### Response

  Returns `200 OK` once the attempt has been transitioned to `completed`.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | Echoes the path parameter. |
  | status | enum | `completed` on success. |
  | credential_id | string \| null | WebAuthn/passkey credential bound to the user at completion. Does not identify or scope the user's EBT. |
  | completed_at | string (date-time) \| null | Completion timestamp. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 404 | No registration attempt with this `registration_attempt_id` exists. |
  | 409 | The attempt is not prepared, the `finalize_token` is unknown, invalid, or expired, or the current state cannot be finalized. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Abort prepared registration

  `POST /api/internal/v1/registrations/{registration_attempt_id}/abort`

  Closes a prepared registration attempt when your IDP fails to commit the passkey on its side. The first successful abort releases the `finalize_token` issued by [prepare-complete](#prepare-registration-completion) and transitions the attempt to `idp_commit_failed` with the supplied `error_code`.

  Use this endpoint after a successful prepare-complete when you need to record an IDP commit failure. If the IDP commit succeeds instead, call [finalize](#finalize-registration) with the same `finalize_token`. To close an in-flight attempt without recording an IDP commit failure, use [cancel](#cancel-registration).

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | The attempt identifier returned by [Start a registration attempt](#start-a-registration-attempt). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | finalize_token | string | yes | The one-time token returned by [prepare-complete](#prepare-registration-completion). |
  | error_code | string | yes | A short machine-readable reason your backend recorded for the IDP commit failure. |

  #### Response

  Returns `200 OK` with the current attempt status; typically `idp_commit_failed` after the first successful abort.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `idp_commit_failed` after a successful abort. |

  #### Errors

  Errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Cancel registration

  `POST /api/internal/v1/registrations/{registration_attempt_id}/cancel`

  Cancels an in-progress registration attempt. Use this endpoint when the user backs out of the enrollment, when your application decides to abandon the attempt, or when your backend detects an upstream condition that makes finishing the attempt unsafe.

  First successful cancel wins; subsequent cancel retries return the prior terminal result, so this endpoint is safe to call as part of cleanup. If a finalize token has already been issued, cancel releases it and marks the attempt `cancelled`; use [abort](#abort-prepared-registration) instead when you need to record an IDP commit failure.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | The attempt identifier returned by [Start a registration attempt](#start-a-registration-attempt). |

  #### Response

  Returns `200 OK` with the attempt's terminal status. Cancel takes no request body.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | registration_attempt_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `cancelled` on the first successful cancel; subsequent calls return the prior terminal status. |

  #### Errors

  Errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

## In-app step-up endpoints
This group covers step-up authentication that happens entirely on the user's mobile device: starting a step-up auth session, starting step-up IDV, polling session status, preparing the IDP-side assertion completion, and finalizing or aborting the prepared completion.

  ### Start a step-up auth session

  `POST /api/internal/v1/step-up/auth-sessions/start`

  Starts an in-app step-up authentication session for an enrolled user. Your backend supplies the IDP-issued passkey assertion challenge bundle; Biometric Passkey creates a step-up session, returns a `km_passkey_session_id`, and your backend then drives the biometric verification and the assertion completion against this session.

  Use this endpoint when your application has decided that a user action requires a high-value step-up, and your backend or IDP has already generated the passkey assertion challenge.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | Idempotency-Key | Recommended | Required for safe retry. Reuse the same key when retrying the same logical step-up. |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | external_user_id | string | yes | Your stable per-user identifier. Must already be enrolled. |
  | passkey_authentication | object | yes | IDP-issued passkey assertion challenge bundle: `challenge`, `rp_id`, `expires_at`. |
  | workflow_input | object | optional | Forwarded to the Entrust Identity Verification workflow run. May carry `custom_data` and `tags`. |

  #### Response

  Returns `201 Created` on a new session; returns `200 OK` on an idempotent replay.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | Opaque session identifier. Use in all subsequent calls in this flow. |
  | status | enum | One of `created`, `idv_in_progress`, `idv_completed`, `auth_finalizing`, `completed`, `failed`, `cancelled`, `expired`. |
  | expires_at | string (date-time) | Session TTL. After this time the session transitions to `expired` automatically. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 404 | The user identified by `external_user_id` is not enrolled. |
  | 409 | An existing active session cannot be replayed in this request context. |
  | 422 | The `passkey_authentication` bundle is inconsistent. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Start in-app step-up IDV

  `POST /api/internal/v1/step-up/auth-sessions/{km_passkey_session_id}/idv/start`

  Starts the on-device biometric verification subphase for an active step-up auth session. Your backend supplies the selected WebAuthn credential identifier and the matching `client_data_hash`; Biometric Passkey returns an Entrust Identity Verification SDK token your backend forwards to the SDK to drive the biometric capture.

  Call this endpoint after the user has selected a passkey credential and the authenticator has produced the `client_data_hash` for the assertion. On success the session transitions to `idv_in_progress`.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | The session identifier returned by [Start a step-up auth session](#start-a-step-up-auth-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | credential_id_hint | string | yes | WebAuthn/passkey credential selector for the assertion flow. Does not identify or scope the user's EBT. |
  | client_data_hash | string | yes | Unpadded base64url encoding of the SHA-256 of the raw clientDataJSON bytes. |

  #### Response

  Returns `200 OK` with the SDK token and the updated session status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `idv_in_progress` after a successful start. |
  | credential_id | string | The credential selected for the assertion flow. Does not identify or scope the user's EBT. |
  | idv | object | `token` and `workflow_run_id` for the in-flight Entrust Identity Verification workflow run. |
  | expires_at | string (date-time) | Session TTL. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 404 | No step-up auth session with this `km_passkey_session_id` exists, or the selected credential is not found or is not active for the session user. |
  | 409 | The session is not in a state that accepts IDV start, no usable EBT is available, or the session binding is incompatible. |
  | 502 | Entrust Identity Verification was unreachable; safe to retry. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Get step-up auth session status

  `GET /api/internal/v1/step-up/auth-sessions/{km_passkey_session_id}`

  Returns the current state of a step-up auth session. Use this endpoint to poll session progress while the on-device biometric verification is running, or to read the bound `credential_id` after the session finishes.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | The session identifier returned by [Start a step-up auth session](#start-a-step-up-auth-session). |

  #### Response

  Returns `200 OK` with the current session snapshot.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | Echoes the path parameter. |
  | external_user_id | string | The user this session belongs to. |
  | status | enum | One of `created`, `idv_in_progress`, `idv_completed`, `auth_finalizing`, `completed`, `failed`, `cancelled`, `expired`. |
  | workflow_run_id | string \| null | Entrust Identity Verification workflow run identifier, when started. |
  | credential_id | string \| null | WebAuthn/passkey credential used for the assertion. Does not identify or scope the user's EBT. |
  | error_code | string \| null | Set on terminal failure states. |
  | expires_at | string (date-time) | Session TTL. |
  | updated_at | string (date-time) | Last modification timestamp on the session. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 404 | No step-up auth session with this `km_passkey_session_id` exists. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Prepare step-up completion

  `POST /api/internal/v1/step-up/auth-sessions/{km_passkey_session_id}/prepare-complete`

  Submits the WebAuthn assertion captured on device for an active step-up auth session. Biometric Passkey records the assertion, transitions the session to `auth_finalizing`, and returns a one-time `finalize_token` for the matching [finalize](#finalize-step-up-authentication) or [abort](#abort-prepared-step-up-authentication) request.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | The session identifier returned by [Start a step-up auth session](#start-a-step-up-auth-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | credential_id | string | yes | WebAuthn/passkey credential returned by the authenticator. Must match the credential selected at IDV start. Does not identify or scope the user's EBT. |
  | client_data_json | string | yes | Unpadded base64url encoding of the raw clientDataJSON bytes. |
  | authenticator_data | string | yes | Unpadded base64url encoding of the raw authenticator data. |
  | signature | string | yes | Unpadded base64url encoding of the raw assertion signature. |

  #### Response

  Returns `200 OK` with the issued `finalize_token` and the updated session status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `auth_finalizing` after a successful prepare-complete. |
  | finalize_token | string | Opaque one-time token consumed by the matching finalize or abort request. |
  | expires_at | string (date-time) | Finalize-token expiry. The `finalize_token` is invalidated at this time. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 404 | No step-up auth session with this `km_passkey_session_id` exists. |
  | 409 | The session is not in a state that accepts a prepare-complete, or the assertion does not match the credential and client-data binding established at IDV start. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Finalize step-up authentication

  `POST /api/internal/v1/step-up/auth-sessions/{km_passkey_session_id}/finalize`

  Completes the Biometric Passkey side of a prepared step-up authentication once your IDP has accepted the assertion. The first successful finalize consumes the `finalize_token` issued by [prepare-complete](#prepare-step-up-completion) and transitions the session to `completed`.

  Call this endpoint after your IDP has accepted the assertion on its side. If the IDP rejects the assertion instead, call [abort](#abort-prepared-step-up-authentication) with the same `finalize_token`. Retrying after the session is terminal returns the current terminal snapshot.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | The session identifier returned by [Start a step-up auth session](#start-a-step-up-auth-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | finalize_token | string | yes | The one-time token returned by [prepare-complete](#prepare-step-up-completion). |
  | credential_id | string | yes | WebAuthn/passkey credential used for the completed assertion. Must match the credential selected at IDV start. |

  #### Response

  Returns `200 OK` once the session has been transitioned to `completed`.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | Echoes the path parameter. |
  | status | enum | `completed` on success. |
  | credential_id | string | WebAuthn/passkey credential used for the completed assertion. Does not identify or scope the user's EBT. |
  | completed_at | string (date-time) \| null | Completion timestamp. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 404 | No step-up auth session with this `km_passkey_session_id` exists. |
  | 409 | The session is not prepared, the `finalize_token` is unknown, invalid, or expired, or the current state cannot be finalized. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Abort prepared step-up authentication

  `POST /api/internal/v1/step-up/auth-sessions/{km_passkey_session_id}/abort`

  Closes a prepared step-up auth session when your IDP rejects the assertion on its side, or when your backend otherwise needs to fail the session after prepare-complete. Releases the `finalize_token` issued by [prepare-complete](#prepare-step-up-completion) and transitions the session to `failed` with the supplied `error_code`.

  Use this endpoint only after a successful prepare-complete. To finalize a successfully accepted assertion instead, call [finalize](#finalize-step-up-authentication) with the same `finalize_token`.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | The session identifier returned by [Start a step-up auth session](#start-a-step-up-auth-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | finalize_token | string | yes | The one-time token returned by [prepare-complete](#prepare-step-up-completion). |
  | error_code | string | yes | A short machine-readable reason your backend recorded for the IDP rejection. |

  #### Response

  Returns `200 OK` with the session's terminal status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | km_passkey_session_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `failed` after a successful abort. |

  #### Errors

  Errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

## Cross-platform step-up endpoints

This group covers step-up authentication that starts in a web browser and hands off to a mobile device for the biometric assertion: starting and polling the cross-platform session, resuming on mobile via the handoff token, starting the mobile auth session and IDV, preparing and finalizing (or aborting) the IDP-side completion, and cancelling the browser/mobile handoff.

  ### Start a cross-platform session

  `POST /api/internal/v1/cross-platform-sessions/start`

  Creates a browser/mobile handoff session for a known Biometric Passkey user and issues a one-time `handoff_token` your backend embeds in the mobile resume callback. Biometric Passkey binds the session to the stable browser ownership tuple (`external_user_id`, `web_context`) so that retries from the same browser context replay the same session deterministically. The active passkey and EBT checks happen later, when the mobile authentication and IDV subphases start.

  Call this endpoint from the browser-side flow as soon as your application has decided that a user action requires a cross-platform step-up. If an active session already exists for the same `external_user_id` and `web_context`, Biometric Passkey returns `200 OK` with the existing `cross_platform_session_id`; the `handoff_token` is omitted on replay. If your backend no longer has the original handoff token, cancel the session or let it expire before starting a new one.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | Idempotency-Key | Recommended | Accepted for safe retry. Replay is anchored to the `(external_user_id, web_context)` tuple rather than to the header alone. |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | external_user_id | string | yes | Your stable per-user identifier. The user must already exist in Biometric Passkey. |
  | web_context | object | yes | Stable browser ownership tuple. Carries `session_id` and optional `actor_context_id`. |
  | continuation_context | string | optional | Opaque caller-owned continuation data echoed back later only to your backend on prepare-complete and resume. |

  #### Response

  Returns `201 Created` on a new session; returns `200 OK` on a replay of an existing active session.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | Opaque session identifier. Use in all subsequent calls in this flow. |
  | status | enum | One of `created`, `handoff_issued`, `mobile_resumed`, `idv_in_progress`, `idv_completed`, `auth_finalizing`, `completed`, `failed`, `expired`, `cancelled`. |
  | handoff_token | string | Opaque one-time mobile handoff token. Present only on newly created sessions; omitted on replay. |
  | expires_at | string (date-time) | Session TTL. After this time the session transitions to `expired` automatically. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 403 | The supplied ownership context is not valid for the session or user; this can include an unknown `external_user_id`. |
  | 409 | The stable browser tuple matches an active session but the supplied continuation context is incompatible. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Get cross-platform session status

  `POST /api/internal/v1/cross-platform-sessions/{cross_platform_session_id}/status`

  Returns the coarse browser-polling state of a cross-platform session. Use this endpoint to drive the browser-side polling UI while the user completes the biometric assertion on the mobile device. The returned `status` is intentionally coarse: `expired` covers both overall session expiry and handoff-token expiry before consumption.

  This endpoint is scoped to the browser ownership tuple: the request body must echo the `external_user_id` and `web_context` that the session was created with.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | The session identifier returned by [Start a cross-platform session](#start-a-cross-platform-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | external_user_id | string | yes | Must match the `external_user_id` the session was created with. |
  | web_context | object | yes | Must match the `web_context` the session was created with. |

  #### Response

  Returns `200 OK` with the current poll snapshot.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | Echoes the path parameter. |
  | status | enum | Coarse poll state: `pending`, `completed`, `failed`, `expired`, `cancelled`. |
  | expires_at | string (date-time) | Cross-platform session TTL. |
  | updated_at | string (date-time) | Last modification timestamp on the session. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 403 | The supplied `external_user_id` or `web_context` does not match the session's ownership tuple. |
  | 404 | No cross-platform session with this `cross_platform_session_id` exists. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Resume cross-platform session on mobile

  `POST /api/internal/v1/cross-platform-sessions/resume`

  Resumes a cross-platform session on the mobile device after your mobile callback has carried the one-time `handoff_token` from the browser leg. Biometric Passkey consumes the handoff token, binds the session to the resuming mobile context, and, when previously stored, echoes the opaque `continuation_context` back to your backend.

  Treat the handoff token as a one-time exchange from your mobile-side backend. The first successful resume consumes the token; later calls with the same, expired, or unknown token are rejected.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | handoff_token | string | yes | The one-time token returned by [Start a cross-platform session](#start-a-cross-platform-session). |

  #### Response

  Returns `200 OK` with the resumed session snapshot.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | Cross-platform session bound to the resumed mobile context. |
  | status | enum | Typically `mobile_resumed` after a successful resume. |
  | expires_at | string (date-time) | Cross-platform session TTL. |
  | continuation_context | string | Optional opaque caller-owned continuation data previously stored on the session. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed `handoff_token`. |
  | 409 | The handoff token is unknown, already consumed, or expired. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Start cross-platform auth session

  `POST /api/internal/v1/cross-platform-sessions/{cross_platform_session_id}/auth-sessions/start`

  Starts the mobile-side step-up auth session once the cross-platform session has been resumed on mobile and your backend or IDP has produced the passkey assertion challenge bundle. Biometric Passkey derives the owner directly from the `cross_platform_session_id`; the caller does not re-assert `external_user_id` on this call.

  Call this endpoint after [Resume cross-platform session on mobile](#resume-cross-platform-session-on-mobile) and after your mobile callback indicates that the real IDP assertion may start. `Idempotency-Key` is accepted; replay still follows the underlying step-up session reuse rules when the `(user_id, challenge_hash)` tuple is compatible.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | Idempotency-Key | Recommended | Required for safe retry. Reuse the same key when retrying the same logical step-up. |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | The session identifier returned by [Start a cross-platform session](#start-a-cross-platform-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | passkey_authentication | object | yes | IDP-issued passkey assertion challenge bundle: `challenge`, `rp_id`, `expires_at`. |
  | workflow_input | object | optional | Forwarded to the Entrust Identity Verification workflow run. May carry `custom_data` and `tags`. |

  #### Response

  Returns `201 Created` on a new auth session; returns `200 OK` on an idempotent replay.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | Echoes the path parameter. |
  | km_passkey_session_id | uuid | The underlying step-up session identifier used for the subsequent IDV, prepare-complete, finalize, and abort calls. |
  | status | enum | Typically `mobile_resumed` immediately after a successful auth-session start. |
  | expires_at | string (date-time) | Underlying step-up auth session expiry. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 403 | The caller is not authorized for this session. |
  | 404 | No cross-platform session with this `cross_platform_session_id` exists. |
  | 409 | The session is not in a state that accepts an auth-session start, or the underlying step-up replay context is incompatible. |
  | 422 | The `passkey_authentication` bundle is inconsistent. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Start cross-platform IDV

  `POST /api/internal/v1/cross-platform-sessions/{cross_platform_session_id}/idv/start`

  Starts the on-device biometric verification subphase for the mobile leg of a cross-platform session. Your backend supplies the selected WebAuthn credential identifier and the matching `client_data_hash`; Biometric Passkey returns an Entrust Identity Verification SDK token your backend forwards to the SDK to drive the biometric capture.

  Call this endpoint after [Start cross-platform auth session](#start-cross-platform-auth-session) and after the user has selected a passkey credential on the mobile device. On success the cross-platform session transitions to `idv_in_progress`.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | The session identifier returned by [Start a cross-platform session](#start-a-cross-platform-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | km_passkey_session_id | uuid | yes | The underlying step-up session identifier returned by [Start cross-platform auth session](#start-cross-platform-auth-session). |
  | credential_id_hint | string | yes | WebAuthn/passkey credential selector for the assertion flow. Does not identify or scope the user's EBT. |
  | client_data_hash | string | yes | Unpadded base64url encoding of the SHA-256 of the raw clientDataJSON bytes. |

  #### Response

  Returns `200 OK` with the SDK token and the updated session status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | Echoes the path parameter. |
  | km_passkey_session_id | uuid | Echoes the underlying step-up session identifier. |
  | status | enum | Typically `idv_in_progress` after a successful start. |
  | credential_id | string | The credential selected for the assertion flow. Does not identify or scope the user's EBT. |
  | idv | object | `token` and `workflow_run_id` for the in-flight Entrust Identity Verification workflow run. |
  | expires_at | string (date-time) | Underlying step-up auth session expiry. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 404 | No cross-platform session with this `cross_platform_session_id` exists, or the selected credential is not found or is not active for the session user. |
  | 409 | The session is not in a state that accepts IDV start, no usable EBT is available, or the session binding is incompatible. |
  | 502 | Entrust Identity Verification was unreachable; safe to retry. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Prepare cross-platform completion

  `POST /api/internal/v1/cross-platform-sessions/{cross_platform_session_id}/prepare-complete`

  Submits the WebAuthn assertion captured on the mobile device for an active cross-platform session. Biometric Passkey records the assertion, transitions the session to `auth_finalizing`, returns a one-time `finalize_token` for the matching [finalize](#finalize-cross-platform-authentication) or [abort](#abort-prepared-cross-platform-authentication) request, and, when previously stored, echoes the opaque `continuation_context` back to your backend.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | The session identifier returned by [Start a cross-platform session](#start-a-cross-platform-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | km_passkey_session_id | uuid | yes | The underlying step-up session identifier returned by [Start cross-platform auth session](#start-cross-platform-auth-session). |
  | credential_id | string | yes | WebAuthn/passkey credential returned by the authenticator. Must match the credential selected at IDV start. |
  | client_data_json | string | yes | Unpadded base64url encoding of the raw clientDataJSON bytes. |
  | authenticator_data | string | yes | Unpadded base64url encoding of the raw authenticator data. |
  | signature | string | yes | Unpadded base64url encoding of the raw assertion signature. |

  #### Response

  Returns `200 OK` with the issued `finalize_token` and the updated session status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | Echoes the path parameter. |
  | km_passkey_session_id | uuid | Echoes the underlying step-up session identifier. |
  | status | enum | Typically `auth_finalizing` after a successful prepare-complete. |
  | finalize_token | string | Opaque one-time token consumed by the matching finalize or abort request. |
  | expires_at | string (date-time) | Finalize-token expiry. The `finalize_token` is invalidated at this time. |
  | continuation_context | string | Optional opaque caller-owned continuation data previously stored on the session. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 404 | No cross-platform session with this `cross_platform_session_id` exists. |
  | 409 | The session is not in a state that accepts a prepare-complete, or the assertion does not match the credential and client-data binding established at IDV start. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Finalize cross-platform authentication

  `POST /api/internal/v1/cross-platform-sessions/{cross_platform_session_id}/finalize`

  Completes the Biometric Passkey side of a prepared cross-platform authentication once your IDP has accepted the assertion. The first successful finalize consumes the `finalize_token` issued by [prepare-cross-platform-completion](#prepare-cross-platform-completion) and transitions the cross-platform session to `completed`.

  Call this endpoint after your IDP has accepted the assertion on its side. If the IDP rejects the assertion instead, call [abort](#abort-prepared-cross-platform-authentication) with the same `finalize_token`. Retrying after the session is terminal returns the current terminal snapshot.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | The session identifier returned by [Start a cross-platform session](#start-a-cross-platform-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | km_passkey_session_id | uuid | yes | The underlying step-up session identifier returned by [Start cross-platform auth session](#start-cross-platform-auth-session). |
  | finalize_token | string | yes | The one-time token returned by [prepare-cross-platform-completion](#prepare-cross-platform-completion). |
  | credential_id | string | yes | WebAuthn/passkey credential used for the completed assertion. Must match the credential selected at IDV start. |

  #### Response

  Returns `200 OK` once the cross-platform session has been transitioned to `completed`.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | Echoes the path parameter. |
  | km_passkey_session_id | uuid | Echoes the underlying step-up session identifier. |
  | status | enum | `completed` on success. |
  | completed_at | string (date-time) \| null | Completion timestamp. |

  #### Errors

  Errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Abort prepared cross-platform authentication

  `POST /api/internal/v1/cross-platform-sessions/{cross_platform_session_id}/abort`

  Closes a prepared cross-platform session when your IDP rejects the assertion on its side, or when your backend otherwise needs to fail the session after prepare-complete. Releases the `finalize_token` issued by [prepare-cross-platform-completion](#prepare-cross-platform-completion) and transitions the session to `failed` with the supplied `error_code`.

  Use this endpoint only after a successful prepare-complete. To finalize a successfully accepted assertion instead, call [finalize](#finalize-cross-platform-authentication) with the same `finalize_token`.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | The session identifier returned by [Start a cross-platform session](#start-a-cross-platform-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | km_passkey_session_id | uuid | yes | The underlying step-up session identifier returned by [Start cross-platform auth session](#start-cross-platform-auth-session). |
  | finalize_token | string | yes | The one-time token returned by [prepare-cross-platform-completion](#prepare-cross-platform-completion). |
  | error_code | string | yes | A short machine-readable reason your backend recorded for the IDP rejection. |

  #### Response

  Returns `200 OK` with the session's terminal status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | Echoes the path parameter. |
  | km_passkey_session_id | uuid | Echoes the underlying step-up session identifier. |
  | status | enum | Typically `failed` after a successful abort. |

  #### Errors

  Errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Cancel cross-platform session

  `POST /api/internal/v1/cross-platform-sessions/{cross_platform_session_id}/cancel`

  Cancels the browser/mobile handoff for the current browser context. Use this endpoint when the user backs out of the browser-side flow, when your application decides to retire the cross-platform session, or when the browser context is being torn down. This call is scoped to the browser ownership tuple: the request body must echo the `external_user_id` and `web_context` the session was created with.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | The session identifier returned by [Start a cross-platform session](#start-a-cross-platform-session). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | external_user_id | string | yes | Must match the `external_user_id` the session was created with. |
  | web_context | object | yes | Must match the `web_context` the session was created with. |

  #### Response

  Returns `200 OK` with the session's terminal status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | cross_platform_session_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `cancelled` after a successful cancel. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 403 | The supplied `external_user_id` or `web_context` does not match the session's ownership tuple. |
  | 404 | No cross-platform session with this `cross_platform_session_id` exists. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

## Recovery endpoints

These endpoints drive the logged-out-device recovery flow: starting a recovery attempt for a known user, polling status, starting recovery IDV, exchanging the recovery token for a post-IDV continuation token, binding a replacement passkey registration, preparing and finalizing (or aborting) the IDP-side completion, and cancelling an in-flight attempt.

  ### Start a recovery attempt

  `POST /api/internal/v1/recovery-attempts/start`

  Starts or replays a logged-out-device recovery attempt for an already-enrolled user identified by `external_user_id`. Biometric Passkey creates the attempt, returns an opaque `recovery_token` your backend uses to start recovery identity verification and exchange continuation context, and returns the attempt and registration TTLs. This endpoint does not accept `Idempotency-Key`; replay is anchored to the active recovery attempt for the same user.

  Use this endpoint when a previously enrolled user signs in on a new device and must re-establish a passkey credential before they can step up. If an active recovery attempt already exists for the same user, Biometric Passkey returns `200 OK` with the existing attempt and a fresh replay of the same `recovery_token`.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | external_user_id | string | yes | Your stable per-user identifier. Must already be enrolled. |

  #### Response

  Returns `201 Created` for a new attempt; returns `200 OK` on replay of an active attempt for the same user.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | Opaque attempt identifier. Use in all subsequent calls in this flow. |
  | status | enum | One of `created`, `idv_in_progress`, `idv_verified`, `registration_in_progress`, `idp_commit_pending`, `completed`, `failed`, `cancelled`, `expired`. |
  | recovery_token | string | Opaque token your backend uses with recovery IDV start and continuation exchange. Do not forward it to the SDK; recovery IDV start returns the SDK `idv.token`. |
  | attempt_expires_at | string (date-time) | Overall attempt TTL. |
  | passkey_registration_expires_at | string (date-time) \| null | Passkey-registration subphase TTL; `null` until the registration subphase has started. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 404 | No Biometric Passkey user or active credential exists for the supplied `external_user_id`. |
  | 409 | An active credential exists but no usable EBT is available for recovery, or enrollment is already in flight for the same user. |
  | 429 | The user has exceeded the recovery-attempt rate or abuse limit; consult the `Retry-After` header. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Get recovery attempt status

  `GET /api/internal/v1/recovery-attempts/{recovery_attempt_id}`

  Returns the current state of a recovery attempt. Use this endpoint to poll attempt progress while recovery identity verification is running, or to read the replacement credential identifier after the attempt finishes.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | The attempt identifier returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Response

  Returns `200 OK` with the current attempt snapshot.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | Echoes the path parameter. |
  | external_user_id | string | The user this attempt belongs to. |
  | status | enum | One of `created`, `idv_in_progress`, `idv_verified`, `registration_in_progress`, `idp_commit_pending`, `completed`, `failed`, `cancelled`, `expired`. |
  | replacement_biometric_passkey_credential_id | uuid \| null | Identifier of the replacement credential bound at completion. |
  | workflow_run_id | string \| null | Entrust Identity Verification workflow run identifier, when started. |
  | error_code | string \| null | Set on terminal failure states. |
  | created_at | string (date-time) | Attempt creation timestamp. |
  | attempt_expires_at | string (date-time) | Overall attempt TTL. |
  | passkey_registration_expires_at | string (date-time) \| null | Passkey-registration subphase TTL. |
  | completed_at | string (date-time) \| null | Completion timestamp on terminal success. |
  | updated_at | string (date-time) | Last modification timestamp on the attempt. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 404 | No recovery attempt with this `recovery_attempt_id` exists. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Start recovery IDV

  `POST /api/internal/v1/recovery-attempts/{recovery_attempt_id}/idv/start`

  Exchanges the `recovery_token` issued at [Start a recovery attempt](#start-a-recovery-attempt) for an Entrust Identity Verification SDK token. Your backend forwards the SDK token to the mobile SDK, which then drives the on-device recovery identity verification capture.

  Call this endpoint once you have received the `recovery_token` and your application is ready to start the on-device capture. The attempt must be in a state that accepts IDV start; on first success the attempt transitions to `idv_in_progress`. If the same call is retried while the attempt is already `idv_in_progress` and the SDK token remains available, Biometric Passkey returns the existing SDK token.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | The attempt identifier returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | recovery_token | string | yes | The opaque token returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Response

  Returns `200 OK` with the SDK token and the updated attempt status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `idv_in_progress` after a successful start. |
  | idv | object | `token` and `workflow_run_id` for the in-flight Entrust Identity Verification workflow run. |
  | attempt_expires_at | string (date-time) | Overall attempt TTL. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 403 | The supplied `recovery_token` does not match the attempt or has been revoked. |
  | 404 | No recovery attempt with this `recovery_attempt_id` exists. |
  | 409 | The attempt is not in a state that accepts or replays an IDV start, the replay SDK token is unavailable, the stored EBT is missing, or the provider applicant binding is missing. |
  | 429 | Rate or abuse limit exceeded; consult the `Retry-After` header. |
  | 502 | Entrust Identity Verification was unreachable; safe to retry. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Exchange recovery continuation

  `POST /api/internal/v1/recovery-attempts/{recovery_attempt_id}/continuation/exchange`

  Exchanges the original `recovery_token` for an opaque post-IDV `continuation_token` once recovery identity verification has succeeded. The exchange also returns the authoritative `external_user_id`, which your backend uses to scope ownership before it issues the replacement passkey registration challenge.

  Call this endpoint after the SDK signals that recovery IDV is complete. If an exchange has already happened, the same `continuation_token` is returned with `200 OK`, so the call is safe to replay.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | The attempt identifier returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | recovery_token | string | yes | The opaque token returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Response

  Returns `201 Created` when the continuation token is issued for the first time; returns `200 OK` on replay of an already-issued continuation token.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | Echoes the path parameter. |
  | external_user_id | string | Authoritative user identifier for owner resolution on the next subphase. |
  | continuation_token | string | Opaque post-IDV token used by [Start recovery registration](#start-recovery-registration) and [Prepare recovery completion](#prepare-recovery-completion). |
  | status | enum | Typically `idv_verified` after a successful exchange. |
  | attempt_expires_at | string (date-time) | Overall attempt TTL. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 403 | The supplied `recovery_token` does not match the attempt or has been revoked. |
  | 404 | No recovery attempt with this `recovery_attempt_id` exists. |
  | 409 | The attempt has not yet completed recovery IDV, or has reached a terminal state. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Start recovery registration

  `POST /api/internal/v1/recovery-attempts/{recovery_attempt_id}/registration/start`

  Binds the replacement passkey registration challenge to the recovery attempt after recovery identity verification has succeeded. Your backend supplies the IDP-issued passkey registration bundle and the post-IDV `continuation_token`; Biometric Passkey records the binding and returns a placeholder `replacement_biometric_passkey_credential_id` for the subsequent prepare-complete and finalize calls.

  Call this endpoint after [Exchange recovery continuation](#exchange-recovery-continuation). On replay of an active recovery registration subphase, the same response is returned with `200 OK`.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | The attempt identifier returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | continuation_token | string | yes | The opaque post-IDV token returned by [Exchange recovery continuation](#exchange-recovery-continuation). |
  | passkey_registration | object | yes | IDP-issued passkey registration challenge bundle: `challenge`, `user_handle`, `rp_id`, `expires_at`. |
  | device | object | optional | Mobile device metadata (`platform`, `device_label`). |

  #### Response

  Returns `201 Created` for the first successful start; returns `200 OK` when the active recovery registration subphase is replayed.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | Echoes the path parameter. |
  | replacement_biometric_passkey_credential_id | uuid | Placeholder identifier reserved for the replacement credential. |
  | continuation_token | string | Echoes the supplied `continuation_token` for traceability. |
  | status | enum | Typically `registration_in_progress` after a successful start. |
  | attempt_expires_at | string (date-time) | Overall attempt TTL. |
  | passkey_registration_expires_at | string (date-time) | Passkey-registration subphase TTL. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 404 | No recovery attempt with this `recovery_attempt_id` exists. |
  | 409 | The attempt is not in a state that accepts a registration start. |
  | 422 | The `passkey_registration` bundle is inconsistent (for example, `expires_at` is in the past). |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Prepare recovery completion

  `POST /api/internal/v1/recovery-attempts/{recovery_attempt_id}/prepare-complete`

  Submits the WebAuthn attestation captured on device for the replacement credential. Biometric Passkey transitions the attempt to `idp_commit_pending` and returns a one-time `finalize_token` for the matching [finalize](#finalize-recovery) or [abort](#abort-prepared-recovery-registration) request.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | The attempt identifier returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | continuation_token | string | yes | The opaque post-IDV token returned by [Exchange recovery continuation](#exchange-recovery-continuation). |
  | attestation_object | string | yes | The raw attestation object from the authenticator (unpadded base64url). |
  | client_data_json | string | yes | Unpadded base64url encoding of the raw clientDataJSON bytes. |

  #### Response

  Returns `200 OK` with the issued `finalize_token` and the updated attempt status.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `idp_commit_pending` after a successful prepare-complete. |
  | finalize_token | string | Opaque one-time token consumed by the matching finalize or abort request. |
  | attempt_expires_at | string (date-time) | Overall attempt TTL. |
  | passkey_registration_expires_at | string (date-time) | Passkey-registration subphase TTL. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 404 | No recovery attempt with this `recovery_attempt_id` exists. |
  | 409 | The attempt is not in a state that accepts a prepare-complete. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Finalize recovery

  `POST /api/internal/v1/recovery-attempts/{recovery_attempt_id}/finalize`

  Completes the Biometric Passkey side of a prepared recovery registration once your IDP has accepted the replacement passkey registration. The first successful finalize consumes the `finalize_token` issued by [prepare-complete](#prepare-recovery-completion) and transitions the attempt to `completed`.

  Call this endpoint after your IDP has committed the replacement credential on its side. If the IDP commit fails instead, call [abort](#abort-prepared-recovery-registration) with the same `finalize_token`. Retrying after the attempt is completed returns the completed snapshot.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | The attempt identifier returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | finalize_token | string | yes | The one-time token returned by [prepare-complete](#prepare-recovery-completion). |

  #### Response

  Returns `200 OK` once the attempt has been transitioned to `completed`.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | Echoes the path parameter. |
  | status | enum | `completed` on success. |
  | credential_id | string \| null | Replacement WebAuthn/passkey credential bound to the user at completion. Does not identify or scope the user's EBT. |
  | completed_at | string (date-time) \| null | Completion timestamp. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 404 | No recovery attempt with this `recovery_attempt_id` exists. |
  | 409 | The attempt is not prepared, the `finalize_token` is unknown, invalid, or expired, or the current state cannot be finalized. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Abort prepared recovery registration

  `POST /api/internal/v1/recovery-attempts/{recovery_attempt_id}/abort`

  Aborts the replacement passkey registration subphase when your IDP fails to commit the replacement passkey on its side. Biometric Passkey releases any issued finalize token, resets the pending replacement credential binding, records the supplied `error_code`, and returns the recovery attempt to `idv_verified` so a new replacement registration can be started.

  Use this endpoint after the recovery registration subphase has started when you want to keep the verified recovery attempt and retry replacement registration. If a valid finalize token has been issued, include it on the abort request. If the IDP commit succeeds instead, call [finalize](#finalize-recovery) with that `finalize_token`. To close the whole recovery attempt, use [cancel](#cancel-recovery-attempt).

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | The attempt identifier returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | finalize_token | string | optional | The one-time token returned by [prepare-complete](#prepare-recovery-completion), when present. |
  | error_code | string | yes | A short machine-readable reason your backend recorded for the IDP commit failure. |

  #### Response

  Returns `200 OK` with the current attempt status after the registration subphase is rewound.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `idv_verified` after a successful abort. |
  | attempt_expires_at | string (date-time) | Overall attempt TTL. |
  | passkey_registration_expires_at | string (date-time) \| null | Passkey-registration subphase TTL. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | Missing or malformed input. |
  | 404 | No recovery attempt with this `recovery_attempt_id` exists. |
  | 409 | The attempt is not in a state that accepts an abort. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Cancel recovery attempt

  `POST /api/internal/v1/recovery-attempts/{recovery_attempt_id}/cancel`

  Cancels the whole in-progress recovery attempt. First successful cancel wins; later cancel retries return the prior terminal result, so this endpoint is safe to call as part of cleanup. Use [abort](#abort-prepared-recovery-registration) instead when you want to keep the verified recovery attempt and retry replacement registration.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | The attempt identifier returned by [Start a recovery attempt](#start-a-recovery-attempt). |

  #### Response

  Returns `200 OK` with the attempt's terminal status. Cancel takes no request body.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | recovery_attempt_id | uuid | Echoes the path parameter. |
  | status | enum | Typically `cancelled` on the first successful cancel; subsequent calls return the prior terminal status. |
  | attempt_expires_at | string (date-time) | Overall attempt TTL. |
  | passkey_registration_expires_at | string (date-time) \| null | Passkey-registration subphase TTL. |
  | completed_at | string (date-time) \| null | Completion timestamp when the attempt has reached a terminal success state on replay. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 404 | No recovery attempt with this `recovery_attempt_id` exists. |
  | 409 | A conflicting cancel or terminal transition is already in flight. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

## User endpoints

The User endpoints let your backend read or repair the Biometric Passkey user profile and its provider-applicant binding outside of an enrollment or recovery flow.

  ### Get user by external user ID

  `GET /api/internal/v1/users/{external_user_id}`

  Returns the Biometric Passkey user profile and the current provider-applicant binding snapshot for the user identified by your stable `external_user_id`. Use this endpoint to confirm a user is enrolled before driving a step-up flow, or to read the bound applicant identifiers for support tooling.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | external_user_id | string | Your stable per-user identifier. |

  #### Response

  Returns `200 OK` when the user is known to Biometric Passkey.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | external_user_id | string | Echoes the path parameter. |
  | first_name | string | Current applicant first name as last bound. |
  | last_name | string | Current applicant last name as last bound. |
  | idv_applicant | object \| null | Current provider-applicant binding summary (`provider_applicant_id`, `first_name`, `last_name`) when bound; `null` if no provider applicant exists yet. |
  | created_at | string (date-time) | User record creation timestamp. |
  | updated_at | string (date-time) | Last modification timestamp on the user record or its provider-applicant binding. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | The `external_user_id` path parameter is missing or malformed. |
  | 404 | No Biometric Passkey user exists for the supplied `external_user_id`. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

  ### Update provider applicant

  `PUT /api/internal/v1/users/{external_user_id}/applicant`

  Repairs or enriches the existing provider applicant bound to the user identified by `external_user_id`. Use this endpoint to correct an applicant field (for example a misspelled name or an updated address) without starting a new enrollment or recovery flow. Biometric Passkey forwards the patch to Entrust Identity Verification for the bound provider applicant.

  Only the fields you include in `applicant` are modified; omitted fields are left untouched. This endpoint does not create a new applicant or rebind the user; if the user has no provider applicant yet, you must enroll or recover the user first.

  #### Request headers

  | Header | Required | Description |
  | --- | --- | --- |
  | X-Correlation-ID | Recommended | Single end-to-end correlation identifier for the logical user action. |

  #### Path parameters

  | Parameter | Type | Description |
  | --- | --- | --- |
  | external_user_id | string | Your stable per-user identifier. |

  #### Request body

  | Parameter | Type | Required | Description |
  | --- | --- | --- | --- |
  | applicant | object | yes | Patch object. Any subset of `first_name`, `last_name`, `dob`, `email`, `phone_number`, `address`. Omitted fields are left untouched. |

  #### Response

  Returns `200 OK` once the patch has been accepted and forwarded to the provider applicant.

  | Attribute | Type | Description |
  | --- | --- | --- |
  | external_user_id | string | Echoes the path parameter. |
  | status | enum | Always `updated`. |
  | updated_at | string (date-time) | Timestamp of the applied update. |

  #### Errors

  | Status | Description |
  | --- | --- |
  | 400 | The path parameter or patch body is malformed. |
  | 404 | No Biometric Passkey user exists for the supplied `external_user_id`. |
  | 409 | The user has no provider applicant bound yet, or a conflicting update is already in flight. |
  | 422 | The patch contains values that fail provider-side validation. |
  | 502 | Entrust Identity Verification was unreachable; safe to retry. |

  All errors return the standard error envelope. Branch on `error.code`; consult `error.retryable`.

## Related guides

- [Biometric Passkey: Version policy](/guide/biometric-passkey-version-policy)
- [Biometric Passkey: SDK integration](/guide/biometric-passkey-sdk)
- [Biometric Passkey: Dashboard and API management](/guide/biometric-passkey-management)