Skip to main content
Beta. The two endpoints on this page are live in Production. Shapes can still change without a major version. iOS is tested end to end. Android is not tested yet. See Android.
The inline passkey lets a person confirm a BotShield request inside your app. The passkey sheet opens over your screen, the person unlocks it the way they unlock their phone, and the request completes. They never leave your app. Nothing about the person changes hands:
  • You learn: only the outcome, verified or a reason it did not happen.
  • You never hold: a BotShield session, a BotShield ID, a verification token from this call, or anything about the person.
  • The passkey stays BotShield’s. It belongs to BotShield’s relying party, botshield.ai. Your app runs the platform’s passkey call with the options BotShield gives you and sends the answer straight back.
You read the result the way you already do: GET /verification/status, the signed token, and the gate.human_verified webhook. Your server code does not change. It is an option. The hand-off to the BotShield app (a link, a QR code or a push) keeps working, and every failure here falls back to it. See Open BotShield.

When it works

The inline passkey runs only when all of these are true: A person who has never used BotShield can’t confirm inline: they create a BotShield ID and passkey in BotShield first. Creating a passkey always happens in BotShield, never in your app.
Not on an Age Gate. The age signal is read by the BotShield app on the person’s phone. Use the Open BotShield hand-off for an Age Gate, as the web widget does.

Web pages

A web page on your own domain can’t run a botshield.ai passkey. Browsers allow it only on botshield.ai origins, and BotShield does not publish a related-origins list for partner domains. On the web, use the widget’s inline passkey (Beta): it runs the passkey in a BotShield frame on your page, on desktop. Phones use the hand-off. A hybrid app (Capacitor, React Native, a WebView) counts as native here. Run the passkey with a native plugin, not with navigator.credentials in the WebView.

The sequence

1

Create the request on your server

Create the request exactly as in Pattern B: POST /sdk/create-session with your API key, then POST /sdk/create-verification-link with the grant token. Send partner_user_ref, the same stable ID the person secured their account with.Return only request_id and web_url to the app. Your API key never goes into the app.
2

Ask BotShield for the passkey options

From the app, call POST /request/passkey-options. It is public: the request_id is the only credential, and it is unguessable and short-lived.
Every answer is HTTP 200, wrapped as { "data": { "data": … } }. Read .data.data. It has one of three shapes.
The request resolves to one person who has passkeys. Run the passkey with options.
An unexpected server error arrives as .data.error with statusCode: 500. Treat it like fallback.
3

Run the passkey on the device

Call the platform’s passkey API with rpId, challenge and allowCredentials from options, and user verification required. On iOS, ask for immediately available credentials only, so a device without the passkey fails fast instead of showing Apple’s “use a passkey from another device” sheet. See the examples.
4

Send the answer back

Call POST /request/passkey-complete with the request, the challenge and the platform’s answer as WebAuthn JSON (AuthenticationResponseJSON).
The answer is { outcome, code? } and nothing else:
BotShield checks that the challenge belongs to this request, that the request is still open and still points at the same person, that the passkey belongs to that person, and that the person unlocked it. Then it completes the request the same way the BotShield app does: the token is minted and your webhook is sent.
5

Finish on your server

Tell your server the app is done, and let the server decide. Read the result by webhook or GET /verification/status, and check the token before you let the action through. Never let the app’s verified alone unlock anything.

Outcomes and what to show

passkey-complete returns one of four outcomes. code is optional and never identifies anyone. The device can fail before BotShield answers. These come from the platform, not from BotShield: Rules that keep it from dead-ending:
  • Every panel keeps a way through: Use the BotShield app instead, which opens url. See Open BotShield.
  • not_linked: offer Link with BotShield ID, then the inline passkey works next time.
  • Coming back from setup_url: call passkey-options again for the same request. When it returns options, the passkey exists now and your button can retry. When it still returns fallback, show “You came back without a passkey.” and turn the button into Open BotShield.
  • Never hardcode BotShield URLs. Use url and setup_url from the response.
  • Never log the challenge, the assertion, the request ID or the credential ID.
Until the next BotShield app release, setup_url opens BotShield on the web, even when the BotShield app is installed. A passkey made there works inline straight away. After that release it opens the BotShield app.

What the person sees

On iPhone, Apple’s sheet reads Sign in to “Your App” with your passkey for “BotShield ID •49ac”. Apple writes the sentence. BotShield sets only the passkey’s name, BotShield ID • and a short suffix, so no email or name appears in your app. Put the Powered by BotShield ID lockup under the button that runs the passkey, and nowhere else. Don’t draw a separate BotShield screen around the sheet.

Examples

Uses @capawesome/capacitor-passkeys. The stock plugin can’t ask iOS for immediately available credentials only, so a device without the passkey shows Apple’s other-device sheet first. Treat CANCELED the same as NO_CREDENTIAL.
Don’t await or return the Passkeys plugin object itself from an async function. Capacitor plugin proxies look like promises, and the call can hang.

Android

Android apps can be registered (see Register your app), and BotShield accepts the app’s signing origin once the registration is approved. No Android app has run the inline passkey end to end yet. Until one has, use the Open BotShield hand-off on Android. An app that tries early and isn’t set up gets failed with assertion_invalid. On Android, use Credential Manager’s GetPublicKeyCredentialOption with the options JSON, and map NoCredentialException to the “no passkey here” panel.

Privacy

  • passkey-options returns WebAuthn options for the request’s person, the hand-off links, and nothing about who the person is. Credential IDs in allowCredentials are opaque to you; don’t store them.
  • passkey-complete returns only outcome and code. It never returns a token, a BotShield ID or a session.
  • The unlock happens on the person’s phone. BotShield receives a signed answer from the passkey, never a face or a fingerprint. See Privacy boundary.