Skip to main content
No dashboard screen — passkeys are driven from your own application. A WebAuthn passkey is registered and used where the end user is present: your app requests options from these endpoints, runs the browser ceremony (navigator.credentials.create() / .get(), or the @simplewebauthn/browser helper), and posts the resulting attestation or assertion back to the matching verify endpoint. The Orbit dashboard covers OTP send/check, profiles, and analytics — it does not enroll or revoke passkeys on a user’s behalf. Retire a lost or rotated key from your app with POST /verify/passkey/factors/{factorId}/revoke.

Worked ceremony sequence

The five endpoints on this page are one lifecycle. The samples below run them in operator order — registration options → browser ceremony → registration verify → login options + login verify → revoke — with the real wire shapes at each step. Every JSON envelope below is a worked copy of the response, not the autogenerated "data": {} hull. Hand the returned options object to the browser verbatim — never re-serialize or mutate fields — and use @simplewebauthn/browser to keep the base64url handling honest.

1. Enroll — registration options

POST /api/v1/verify/passkey/registration/options returns the PublicKeyCredentialCreationOptionsJSON and a challengeId you submit back with the attestation.
The options endpoints are rate-limited at 20 requests/minute per tenant.

2. Enroll — browser ceremony + attestation verify

In the browser, run the ceremony with the exact options payload from step 1, then POST the serialized credential back within the TTL (default 300s, capped at 600):
POST success returns 201 (not 200). backupEligible + backupState echo the authenticator’s BE/BS flags: false on backupEligible means the key lives on this one device — treat it accordingly.

3. Login — authentication options

POST /api/v1/verify/passkey/authentication/options for the identity returns PublicKeyCredentialRequestOptionsJSON with that identity’s credentials hinted in allowCredentials:

4. Login — browser ceremony + assertion verify

On a passkey-counter regression — the W3C cloning signal — the factor is auto-revoked and the response carries cloningSuspected: true. The verify endpoints are rate-limited at 60 requests/minute per tenant.

5. Retire — revoke

POST /api/v1/verify/passkey/factors/{factorId}/revoke flips the factor to revoked. Idempotent — re-calling an already-revoked factor returns the same response:
After revocation, new login ceremonies against the factor are rejected with 409 / reason: "FACTOR_REVOKED".

Relying party and origin

WebAuthn binds every credential to a relying-party ID and an HTTPS origin, and the returned options payload carries both. The default RP is rpId: "verify.devotel.io" with origin https://verify.devotel.io.
  • Production apps on their own domain must be issued options whose rp.id is a registrable suffix of the page origin — browsers reject the ceremony otherwise, no network call ever fires. rpId must equal the origin’s host or be a parent domain of it (app.acme.com may use acme.com).
  • Local development: run on http://localhostlocalhost is exempt from the HTTPS rule and ceremonies work there.
  • Never mutate the options object between the API response and the browser call; the attestation/assertion is bound to the exact challenge, RP ID, and origin inside it.

Server-side without an SDK helper

The Node SDK ships no typed passkey helper — walk the same ceremony server-side (test harnesses, CLI, CI smoke) with the generic orbit.request escape hatch:
The browser ceremony step itself cannot move server-side — the authenticator lives with the user. Everything before and after it can.

Error surface

Failures arrive in the standard error envelope (meta.request_id included). Plan for these on the ceremony paths: A 422 on registration verify marks the challenge denied, so a malformed retry does not silently burn the pending ceremony — re-issue fresh options instead of looping on the same challengeId. Every passkey endpoint is authentication- and rate-limit-gated only — no extra scope — so issue dedicated keys for factor flows and restrict which callers can reach them. A factor created by one tenant is never visible to another.