Skip to content
Akedly
Passkeys

Passkeys on V2.0

Add passkeys to the widget so returning users verify with their device instead of a code.

Passkeys

Let returning users verify with the biometric that already unlocks their device — Face ID, Touch ID, Windows Hello, or a screen lock — instead of waiting for an OTP. Built on the WebAuthn standard and layered directly onto the V2 Widget you already know: same attempt lifecycle, same result contracts, one optional change to your embed. There is no shared secret to phish, intercept, or replay.

How passkeys work

Passkeys are woven into the existing widget flow — the user is never stranded on a passkey screen.

  1. Captcha. The user passes the standard Cloudflare Turnstile check, exactly as today.
  2. Returning user with a passkey on this device. If the bound phone already has a passkey on this device, the widget offers an optional "Use passkey" button. "Use a code instead" is always shown alongside it.
  3. Everyone else goes straight to normal OTP — no extra screens.
  4. Success is identical to OTP — same redirect, same webhook contract, same postMessage.
  5. After an OTP success, the widget may show an optional, skippable "Enable passkey" prompt so the next verification is instant.

V2 ceremonies ask the device for user verification as preferred; the V1.2 passkey ceremony requires it.

These short clips show both passkey ceremonies from the user's side:

Registering a passkey

First-time passkey registration in the widget.

Open the video in Google Drive

Verifying with a saved passkey

Returning-user passkey verification in the widget.

Open the video in Google Drive


New integrator guide

New to Akedly? Don't start here. Passkeys are an enhancement of the V2 Widget, not a standalone product — set the widget up first, and passkeys ride on the same attempt lifecycle with no extra steps.


Already using V2? Here's the one change

If you already run the V2 Widget, enabling passkeys is a single attribute on your iframe. Add the allow attribute so the embedded widget is permitted to invoke WebAuthn:

html
allow="publickey-credentials-get *; publickey-credentials-create *"

That's it. Here is the before → after for each common embed.

Add the allow attribute

<!-- Before -->
<iframe src="https://auth.akedly.io/auth?attemptId=..."></iframe>

<!-- After -->
<iframe
  src="https://auth.akedly.io/auth?attemptId=..."
  allow="publickey-credentials-get *; publickey-credentials-create *"
></iframe>

Enabling passkeys for your account

Passkey availability depends on the current production configuration, account entitlement and pipeline settings. The checked-in backend defaults are not a promise about production access. Three gates decide whether the widget offers passkeys:

  • Name
    Global rollout
    Type
    Akedly-controlled
    Description
    Passkeys are generally available. Akedly controls the global rollout switch.
  • Name
    Account entitlement
    Type
    account access
    Description
    Granted automatically on the unverified-to-verified transition, so a verified account normally has it. The grant is transition-only: an account that has never verified receives nothing automatically, and one whose entitlement an admin revoked does not get it back by verifying again. An admin can also grant or revoke it directly, independently of verification.
  • Name
    Passkeys
    Type
    pipeline toggle
    Description
    The dashboard exposes the pipeline setting as Passkeys. New pipelines are created with passkeys on; existing pipelines keep their current setting.
TrackSurfaceEntitlementAvailability
V1Pipeline editor / direct RESTNot applicableNo direct V1 passkey ceremony. The shared pipeline field does not add a V1 passkey route.
V1WidgetAll three gatesSupported through the widget when global rollout, account entitlement, and the linked Passkeys setting are true.
V1.2Pipeline editor / direct RESTAll three gatesSupported through /api/v1.2/transactions/passkey when the pipeline is configured for V1.2 and all three gates are true.
V1.2WidgetNot applicableUnavailable: widget attachment rejects V1.2 pipelines.
V2Pipeline editorAll three gatesThe editor controls the linked Passkeys setting; the runtime passkey path is the widget.
V2WidgetAll three gatesSupported through the widget when all three gates are true; widget validation exposes featureFlags.passkey_enabled.

The pipeline control lives on the pipeline page, described in-product as "Let returning users verify with a device passkey and brand the experience":

Once passkeys are live, enrolled credentials and passkey activity are visible under Monitoring → Authentication → Passkeys (/admin/passkeys) in the dashboard:


Platforms & surfaces

What you change depends only on how you embed the widget. There are no new API routes, parameters, or SDK upgrades — the widget runs the passkey ceremonies itself.

Add the allow attribute to your iframe — one line, done. See the before → after snippets above. Without it, the widget runs OTP as today; with it, passkeys appear when enabled for the account.


Handling the result

The result contracts are identical to standard V2 — consumers should key only on type === "AUTH_SUCCESS" and tolerate extra optional fields. AUTH_SUCCESS is an optional hint: with a web Front-end Redirect URL the widget sends the user to that URL from inside the widget frame and does not post it, so treat the backend webhook, or a status lookup, as the source of truth.

postMessage

  • Name
    type
    Type
    string
    Description

    "AUTH_SUCCESS". Key on this and nothing else — tolerate any extra optional fields.

  • Name
    attemptId
    Type
    string
    Description

    The original attemptId you created.

  • Name
    transactionId
    Type
    string
    Description

    The transaction identifier for this verification.

  • Name
    redirectUrl
    Type
    string
    Description

    Optional. Present when a frontend callback URL is configured.

  • Name
    timestamp
    Type
    string
    Description

    ISO 8601 timestamp of when authentication completed.

postMessage (passkey success)

{
  "type": "AUTH_SUCCESS",
  "attemptId": "attempt_a1b2c3d4e5f6...",
  "transactionId": "pkreq_9f8e7d6c5b4a...",
  "redirectUrl": "https://yourapp.com/auth/callback?status=success&...",
  "timestamp": "2026-01-16T12:05:30.123Z"
}

Redirect

The success redirect query string is unchanged:

text
…?status=success&transactionId=…&attemptId=…&timestamp=…&meta_*

Webhook signing

The webhook envelope is byte-compatible with the standard V2 webhook — same shape and signing mechanism. Signing is best-effort for widget callbacks: headers are present when signing succeeds, but a callback is still sent if signing fails. For passkey verifications, note the following:

  • Name
    transaction.transactionID
    Type
    string
    Description

    For a passkey verification this is an opaque passkey request id, not an OTP transaction id. Treat it as opaque — store it and echo it back, but don't parse or assume its format.

  • Name
    transaction.verificationMethod
    Type
    string
    Description

    Optional. "passkey" when the user verified with a passkey.

  • Name
    transaction.resolvedChannel / transaction.channels
    Type
    string / string[]
    Description

    Absent for passkey verifications — there is no OTP delivery channel (WhatsApp / SMS / Email) involved. Don't depend on these fields being present.


Local vs on-device testing

Where the ceremony runs — and therefore the relying-party (RP) ID the passkey binds to — is controlled by ceremonyOrigin:

  • Established local QA points the ceremony at the shared-cert dev auth-gateway origin (https://localhost:5174), binding to RP=localhost, which iOS Simulators and Android emulators accept. If the shared certificate files are absent, the dev server falls back to HTTP; configure ceremonyOrigin to the exact origin the server prints rather than mixing HTTP and HTTPS.
  • Production / real devices use https://auth.akedly.io, binding to RP=akedly.io.

Per-platform prerequisites for the local loop: iOS Simulator → iOS 16+ and signed into iCloud; Android emulator → Google Play Services plus a configured screen lock (Credential Manager refuses without one); Web → any WebAuthn-capable browser.


Debugging tips


FAQ & gotchas


Support & resources