Register a passkey

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

Finishes a registration started with POST /fapi/v1/me/passkeys/registration-options: verifies the browser's answer, stores the passkey or security key, and returns it. The user receives a security email about the new sign-in method; this email cannot be turned off. 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).

The session rules are those of the start call: verified in the last 10 minutes, and opened with a second factor if the user has one.

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
ceremony_id string yes The ceremony_id of the start call.
credential object yes The result of navigator.credentials.create as JSON (PublicKeyCredential.toJSON()), at most 16 KiB.

Response

201 Created, the new authenticator:

{
  "id": "cred_01j9zzzzzzzzzzzzzzzzzzzzzz",
  "type": "passkey",
  "label": "iCloud Keychain",
  "status": "active",
  "synced": true,
  "created_at": "2026-10-12T09:00:00Z",
  "last_used_at": null
}

Errors

A refused registration is not counted as a failed attempt; start again with a new registration.

Status Code When
400 malformed_request The body is not valid JSON.
400 unknown_field The body has a field other than ceremony_id and credential.
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 values in the WWW-Authenticate header.
403 origin_not_allowed Origin is missing or wrong.
409 credential_already_registered This authenticator is already registered in the environment.
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 /ceremony_id: the registration is unknown, expired, already used or another session's. /credential: the answer is missing or does not verify.
422 device_bound_passkey_required The environment accepts only passkeys that stay on one device.
422 aaguid_not_allowed The environment does not allow this authenticator model.
422 attestation_untrusted The environment requires a trusted attestation and this one is not.

Example

curl -X POST https://example.keenauth.app/fapi/v1/me/passkeys \
  -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 '{"ceremony_id": "CQAAAAAAAAAAAAAAAAAAAA", "credential": {"id": "…", "rawId": "…", "type": "public-key", "response": {"clientDataJSON": "…", "attestationObject": "…"}}}'

Personal data

This endpoint carries personal data: credentials. Personal data in the KeenAuth APIs 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.