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.
2. Enroll — browser ceremony + attestation verify
In the browser, run the ceremony with the exactoptions 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
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:
409 / reason: "FACTOR_REVOKED".
Relying party and origin
WebAuthn binds every credential to a relying-party ID and an HTTPS origin, and the returnedoptions 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.idis a registrable suffix of the page origin — browsers reject the ceremony otherwise, no network call ever fires.rpIdmust equal the origin’s host or be a parent domain of it (app.acme.commay useacme.com). - Local development: run on
http://localhost—localhostis exempt from the HTTPS rule and ceremonies work there. - Never mutate the
optionsobject 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 genericorbit.request
escape hatch:
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/optionsstring
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/verifystring
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}/revokerevoked. 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/optionsstring
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/verifystring
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.