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.
- You learn: only the outcome,
verifiedor 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.
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.
Web pages
A web page on your own domain can’t run abotshield.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 Every answer is HTTP An unexpected server error arrives as
POST /request/passkey-options. It is public: the request_id is the only credential, and it is unguessable and short-lived.200, wrapped as { "data": { "data": … } }. Read .data.data. It has one of three shapes.- options
- fallback
- closed
The request resolves to one person who has passkeys. Run the passkey with
options..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 The answer is 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.
POST /request/passkey-complete with the request, the challenge and the platform’s answer as WebAuthn JSON (AuthenticationResponseJSON).{ outcome, code? } and nothing else: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: callpasskey-optionsagain for the same request. When it returnsoptions, the passkey exists now and your button can retry. When it still returnsfallback, show “You came back without a passkey.” and turn the button into Open BotShield. - Never hardcode BotShield URLs. Use
urlandsetup_urlfrom 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
- Capacitor (iOS)
- Swift (iOS 16+)
- Web (widget)
Uses Don’t
@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.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 getsfailed 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-optionsreturns WebAuthn options for the request’s person, the hand-off links, and nothing about who the person is. Credential IDs inallowCredentialsare opaque to you; don’t store them.passkey-completereturns onlyoutcomeandcode. 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.
