
# Start a passkey registration

`POST /fapi/v1/me/passkeys/registration-options` · called from the environment's sign-in host

Opens a WebAuthn registration for the authenticated user from the account portal and returns the options to pass to `navigator.credentials.create`. Finish it with [`POST /fapi/v1/me/passkeys`](./register). The registration is valid for 5 minutes, can be finished once, and only by the session that started it. Requires the authenticated user's `__Host-idp_session` cookie; if the cookie is missing, expired, or revoked, returns `401 unauthenticated`. A first-party app on another origin can instead send its front API access token (`Authorization: DPoP <token>` with a DPoP proof; see [Front API access token](/docs/api/fapi/front-api-token)).

The session must have been verified in the last 10 minutes for a passkey, or in the last 5 minutes for a security key. If the user already has a second factor, the session must also have been opened with a second factor (`urn:idp:acr:mfa`): a new passkey cannot raise the assurance of the session that adds it.

## Request

Headers:

| Header | Required | Description |
| --- | --- | --- |
| `Idp-Client` | yes | SDK identifier. |
| `Origin` | yes | Must be exactly `https://<sign-in host>`. |
| `Content-Type` | yes | `application/json`. |

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `role` | string | yes | `passkey`: a credential that signs in on its own (needs passkey sign-in to be on in the environment). `security_key`: a credential used only as a second factor (needs the security key second factor to be on). |

## Response

`200 OK`:

```json
{
  "ceremony_id": "CQAAAAAAAAAAAAAAAAAAAA",
  "options": {"publicKey": {"challenge": "…", "rp": {"id": "example.keenauth.app", "name": "Example"}, "user": {"id": "…", "name": "ada@example.org", "displayName": "ada@example.org"}, "pubKeyCredParams": [], "timeout": 300000, "excludeCredentials": [], "authenticatorSelection": {}, "attestation": "none", "extensions": {"credProps": true}}}
}
```

Pass `options.publicKey` to `PublicKeyCredential.parseCreationOptionsFromJSON` and the result to `navigator.credentials.create`. `excludeCredentials` lists the user's credentials for this relying party, so the same authenticator is not registered twice.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `malformed_request` | The body is not valid JSON. |
| 400 | `unknown_field` | The body has a field other than `role`. |
| 401 | `unauthenticated` | No valid session cookie. |
| 401 | `step_up_required` | The session is too old or a second factor is required; re-verify with the `max_age` (and if applicable, `acr_values`) in the `WWW-Authenticate` header. |
| 403 | `origin_not_allowed` | `Origin` is missing or wrong. |
| 403 | `no_allowed_method` | The environment does not allow this `role`. |
| 409 | `credential_limit_reached` | The user already has the maximum number of passkeys and security keys. |
| 413 | `payload_too_large` | The body is too large. |
| 415 | `unsupported_media_type` | `Content-Type` is not `application/json`. |
| 422 | `validation_failed` | `role` is missing or not `passkey` or `security_key`. |
| 422 | `aaguid_not_allowed` | The environment's authenticator allowlist is invalid; registration is blocked until it is fixed. |

## Example

```bash
curl -X POST https://example.keenauth.app/fapi/v1/me/passkeys/registration-options \
  -H "Idp-Client: my-app/1.0.0 (node/22.0.0)" \
  -H "Origin: https://example.keenauth.app" \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{"role": "passkey"}'
```

## Personal data

This endpoint carries personal data: credentials. [Personal data in the KeenAuth APIs](/docs/policies/personal-data) describes what is stored, how long it is kept and how it is deleted. Do not send special categories of personal data (health, biometric data, religion, political opinions, sexual orientation) in any field.
