Skip to main content

Verify Passkey API

Verify Passkey endpoints exposed by the Devotel CPaaS API Base path: /api/v1/verify/passkey Endpoint count: 5
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://localhost — localhost 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.

Issue WebAuthn authentication options

POST /api/v1/verify/passkey/authentication/options
Returns PublicKeyCredentialRequestOptionsJSON the browser hands to navigator.credentials.get(). allowCredentials includes the identity’s registered passkeys with their transport hints.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
string
required
The end-user identifier whose registered passkeys should be offered for this authentication ceremony (1–255 characters).
integer
Lifetime of the returned authentication challenge, in seconds, before it expires. Defaults to 300 (5 minutes); range 30–600.

Verify WebAuthn authentication assertion

POST /api/v1/verify/passkey/authentication/verify
Verifies the assertion returned by navigator.credentials.get(). On success bumps counter + lastUsedAt. Counter regression auto-revokes the factor (W3C WebAuthn L3 § 6.1.3 cloning signal).
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
string
required
The challengeId returned by POST /verify/passkey/authentication/options.
object
required
The AuthenticationResponseJSON returned by the browser’s navigator.credentials.get() (or @simplewebauthn/browser’s startAuthentication()), serialized to JSON.

Revoke a passkey factor

POST /api/v1/verify/passkey/factors/{factorId}/revoke
Flips a passkey factor to revoked. Idempotent — a second call on an already-revoked factor returns the same response.
string
required
—
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.

Issue WebAuthn registration options

POST /api/v1/verify/passkey/registration/options
Returns PublicKeyCredentialCreationOptionsJSON the browser hands to navigator.credentials.create(). The resulting attestation must be submitted to /verify/passkey/registration/verify within the TTL.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
string
required
Stable identifier for the end user enrolling this passkey — your user id, email, or username (1–255 characters).
string
required
Human-readable label for the credential, shown in the operating-system prompt during enrollment (1–120 characters).
integer
Lifetime of the returned registration challenge, in seconds, before it expires. Defaults to 300 (5 minutes); range 30–600.

Verify WebAuthn registration attestation

POST /api/v1/verify/passkey/registration/verify
Verifies the attestation returned by navigator.credentials.create(). On success creates a verify_passkey_factor row from the credential public key + counter + transports.
string
Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the Idempotency-Replay: true response header.
string (enum: true|false)
Sandbox opt-in for Clerk-session-authenticated requests. Set to true to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response meta.test_mode: true. Ignored for live API keys (dv_live_sk_*) — server-to-server clients must use a test-prefixed key (dv_test_sk_*) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
string
required
The challengeId returned by POST /verify/passkey/registration/options.
object
required
The RegistrationResponseJSON returned by the browser’s navigator.credentials.create() (or @simplewebauthn/browser’s startRegistration()), serialized to JSON.