Skip to main content
A result in the browser is a hint. The decision belongs on your server. There are two patterns:
  • Pattern A: verify the widget’s result. The web component runs the verification and your server checks the token or request ID it produced.
  • Pattern B: server-to-server. Your server creates the verification request, you show the link in your own UI, and you learn the result by webhook or polling.
All calls go to https://api.botshield.ai/operations. Every operation answers HTTP 200 with a data envelope, and handler errors arrive inside it as data.error, also with HTTP 200. Check data.error before you read the payload. See API overview and Errors.
TypeScript SDK. Pass serverURL when you construct the client; the package does not default to production. apiKeyAuth is sent as the Authorization header exactly as you give it, so include the Bearer prefix. The SDK does not unwrap the envelope, and it uses camelCase field names. See TypeScript SDK.

Pattern A: verify the widget’s result

The widget’s botshield:success event gives your page a request_id and a token. The token is a signed JWT or null; it is never a request ID. Send both to your server with the action they protect.

The attestation token

The token describes the event, never the person. It has no subject, no user ID and no email. It also has no gate claim: see What to check.

Verify the token

POST /sdk/verify-token needs no API key. The token is the proof.
(census is the API’s name for BotShield Gate.)A valid token:
An expired or invalid token still answers HTTP 200, with valid: false:
For a bad signature, reason is "Invalid token — signature verification failed" and there are no claims.

When there is no token

On a gate in Recent Presence mode, a returning human can pass without a phone step. The widget’s success event then has token: null, via: "precheck" and a request_id. Confirm it from your server with GET /verification/status, which needs no API key:
A fast-path record reads as pass for 60 seconds, then as expired. Call the status endpoint as soon as your server receives the request_id. Accept completed or pass as Verified, and never test for completed alone. If the record already reads expired, ask the user to verify again. No webhook is sent for a fast-path pass.
If your server must hold a signed token for every verification, set the gate to Live. A Live gate always produces a token.

What to check

verify-token needs no credentials and is not bound to an audience. It answers valid: true for a genuine token issued for any organization, and it does not consume the token, which stays valid for its full 120 seconds. The same is true of a local signature check. So a valid signature is only the first test. Before you let the action through, confirm all of these on your server:
  1. The token verifies (signature, issuer, not expired), or the status is completed or pass.
  2. organization_id is yours. Copy your Organization ID from Settings → Developer Tools → API Keys in the Console and keep it in your configuration. A create-session response carries the same value as organization.id.
  3. The request_id belongs to this action. In Pattern B, compare it with the request you created. In both patterns, record every request_id you accept and reject repeats, so one verification cannot be replayed for many actions.
  4. The gate is the one you expect. The token carries no gate claim, so if you run several gates, or both environments, read GET /verification/status and compare scope with your gate key and metadata.environment with production.

Pattern B: server-to-server

Use this when you draw your own UI, for a native app, a kiosk, or a back-office flow. Every request runs the live check on the user’s phone; the Recent Presence fast path is a widget feature. Server-to-server verifications count in the gate’s Overview metrics in the Console, the same as widget verifications.
1

Create a session

Call POST /sdk/create-session with your API key: bs_dev_… for Development gates, bs_production_… for Production gates. BotShield resolves the gate in the environment of the key that calls it, so the same gate key can exist in Development and in Production as two independent gates.
The same three values are also returned under the older names session_token, expires_at and expires_in_seconds. The grant token lives 5 minutes and works once: the next step consumes it. It only authorizes creating one verification request; it is not a user session.
2

Create the verification request

Call POST /sdk/create-verification-link with the grant token as the Bearer credential, not your API key.
Errors arrive as data.data.error with HTTP 200:
3

Show the link to the user

On desktop, render web_url as a QR code with your own QR library and ask the user to scan it with their phone camera. On mobile, open web_url or deep_link. The user needs a BotShield ID — web_url opens the BotShield web app (public beta), where a first-time user creates a passkey with no download; deep_link opens the BotShield app if they have it. They confirm with their device biometric and are handed back to what they were doing.
4

Learn the result

Add an endpoint in the Console under Settings → Developer Tools → Webhooks and subscribe to gate.human_verified and gate.unavailable. Match the delivery to your request by request_id.
One endpoint receives deliveries from both environments, so branch on the top-level environment (development or production). metadata holds only the keys you supplied. Verify the signature on every delivery, and treat deliveries as at-least-once. See Webhooks and Webhook events for the full payloads and the gate.unavailable reasons.
Keep your own timer. BotShield sends gate.unavailable when the BotShield app reports a failure, for example when the person cancels the biometric prompt, and when a request that was opened fails. If the user never opens the link, the request simply expires and no webhook is sent. When expires_at passes with no gate.human_verified, treat the request as Unavailable.

Status values

verification/status returns its payload directly under data, with no second data level. The status response never contains identity fields. It reports the request, not the person.

The 409 case

BotShield allows one pending request per user and gate. The user is the partner_user_ref you send to create-verification-link, or else the partner_user_id you sent to create-session. While that user has a pending, unexpired request for the gate, a second create-verification-link for the same gate returns:
If the user abandoned the first request, revoke it with your API key and start again from create-session (the grant token you used for the rejected call is still unused, so you can also reuse it within its 5 minutes).
Revoking expires the pending requests immediately. revoke-verification clears exactly the requests the 409 rule matches, the pending, unexpired ones for that user and gate, and returns how many in revoked_count. Pass the same identifier in partner_user_id that the request was created with. A completed or failed request never blocks a new one, so you can ask the same user to verify at the same gate again as soon as the last request finishes. The rule applies only when the request names a user: leave partner_user_id off create-session and partner_user_ref off create-verification-link if you do not need it. It also does not apply to requests the web component makes with a site key.

Next steps

Webhooks

Add an endpoint and verify deliveries.

TypeScript SDK

Install and configure botshield-sdk.

Age Gate

Read the age result on your server.

Errors

The error envelope and status codes.