Akedly

Passkeys (WebAuthn) for V2 Widgets

Passkeys let your users verify with the biometric they already use to unlock their device — Face ID, Touch ID, Windows Hello, or a screen lock — instead of waiting for a one-time code. Built on the WebAuthn standard and layered directly onto the V2 Widget you already know.

Instant verification — no code to wait for. When a returning user has a passkey on their device, the widget skips OTP delivery entirely: one biometric prompt and they're verified.


Why passkeys

Faster — instant. No SMS or WhatsApp round-trip. A returning user with a passkey on their device verifies in a single biometric prompt, with nothing to type and no code to wait for.

Higher conversion. Removing the "wait for a code, switch apps, copy, paste" loop eliminates the biggest source of drop-off in phone verification.

More secure. Passkeys are device-bound public-key credentials. There is no shared secret to phish, intercept, or replay — the private key never leaves the user's device.

Lower cost. A successful passkey authentication is billed at a fraction — 60% by default — of that end user's frozen anchor rate. Enrollment is free, and only a successful authentication is ever charged.

Passkeys end to end

A commentary walkthrough of the passkey experience.


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:

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>

How it works (end-user flow)

The passkey experience is woven into the existing widget flow. The user is never trapped on a passkey screen — a code is always one tap away.

  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 number already has a passkey on this device, the widget offers an optional "Use passkey" button. A "Use a code instead" option is always present alongside it.
  3. Everyone else. If there's no passkey for this device, the widget goes straight to normal OTP — no extra screens.
  4. Success. A passkey verification completes the attempt just like an OTP verification: same redirect, same webhook, same postMessage.
  5. After an OTP success. Once a user verifies with a code, the widget may show an optional, skippable "Enable passkey" prompt so their next verification can be instant.

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.

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

Registering a passkey

First-time passkey registration in the widget.

Verifying with a saved passkey

Returning-user passkey verification in the widget.

Handling the result

The result contracts are identical to standard V2 — consumers should key only on type === "AUTH_SUCCESS" and tolerate extra optional fields. A passkey success may add one optional field, verificationMethod: "passkey".

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.

  • Name
    verificationMethod
    Type
    string
    Description

    Optional. "passkey" when the user verified with a passkey. Absent for OTP.

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",
  "verificationMethod": "passkey"
}

Redirect

The success redirect query string is unchanged:

…?status=success&transactionId=…&attemptId=…×tamp=…&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
    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
    verificationMethod
    Type
    string
    Description

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

  • Name
    channel
    Type
    string
    Description

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


Platforms & surfaces

What you edit depends on how you embed the widget. The short version: web iframes add the allow attribute, full-page redirects need nothing, and mobile native apps must open the widget in a system browser — not a WebView (see below).


Enabling passkeys

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:


Passkeys on V1.2

V1.2 passkeys now have a dedicated guide.


FAQ & gotchas


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.


Support & resources

Was this page helpful?