> ## Documentation Index
> Fetch the complete documentation index at: https://docs.botshield.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Web component

> Reference for the botshield-verify element: attributes, events, methods, theming and framework notes.

`<botshield-verify>` is a custom element that draws the **Verify Human** button on your page, runs the verification, and reports the result to your code. It renders in a closed Shadow DOM, so your page styles do not leak in. You need a site key and an active gate first: see [Place a gate](/gate/place-a-gate).

## Install

Load the script once per page, then add the element where the button should appear.

```html theme={null}
<script src="https://cdn.botshield.ai/sdk.js"></script>

<botshield-verify
  site-key="pk_live_…"
  scope="checkout"
  scan-mode="modal"
></botshield-verify>
```

<Note>
  `scan-mode="modal"` names the widget's flow: a QR code modal on desktop and a hand-off to the BotShield app on mobile. It is the default, so the widget runs the same flow when the attribute is absent. The Console's embed snippet includes it, and so do the samples on this page.
</Note>

The script registers the element and sets `window.BotShield`. This URL serves the latest stable version. To pin a version, see [Widget versions](/gate/widget-versions). Load it with a plain `<script src>` tag so the widget can tell which host it came from.

## Attributes

| Attribute | Required | Default | Description |
| - | - | - | - |
| `site-key` | Yes | — | Your public site key, `pk_test_…` or `pk_live_…`. The page origin must be in the key's allowed origins. |
| `scope` | Yes | — | The gate **Key** from the Console, case-sensitive. (`scope` is the API's name for the gate key.) |
| `scan-mode` | No | `modal` | The verification flow. `modal` is the only value. Any other value also runs the modal flow, and the widget logs one console warning. |
| `platform-user-ref` | No | — | Your own stable identifier for the signed-in user, for example your internal user ID. It enables [Recent Presence and push](#returning-users). BotShield stores it only as a one-way hash. Prefer an opaque ID over an email address. |
| `link-on-verify` | No | `true` | When a verification succeeds, BotShield remembers that `platform-user-ref` belongs to this human so later visits can use Recent Presence and push. Set `false` to opt out. |
| `theme` | No | `auto` | `light`, `dark` or `auto`. `auto` follows the visitor's `prefers-color-scheme`. |
| `checkout-label` | No | `Checkout` | Label for the [action button](#the-action-button) the widget draws under the verify button. |
| `checkout` | No | `true` | Set `false` to hide the [action button](#the-action-button), for example when you have your own submit button. |
| `onsuccess` | No | — | Name of a global function to call on success. It receives the same object as the `botshield:success` event `detail`. |
| `onfailure` | No | — | Name of a global function to call on failure. It receives the same object as the `botshield:failure` event `detail`. |
| `betas` | No | — | Comma-separated beta features. The only value is [`inline-passkey`](#inline-passkey-beta). |
| `mode` | No | `private` | Leave unset. `private` means the user confirms with a passkey and no account sign-in. `linked-account` is also accepted. Any other value makes the request fail with `modal_create_failed`. |
| `state` | Output | `idle` | Set **by the widget**: `idle`, `verifying`, `verified` or `failed`. Read it or style against it; do not write it. |

`site-key` is read when the element connects to the page. The other attributes are read when the user clicks, so you can update `platform-user-ref` after sign-in without re-creating the element.

## What the user sees

The button title is always "Verify you're human". The status line changes with the state:

| `state` | Status line | Meaning |
| - | - | - |
| `idle` | Tap to confirm | Waiting for a click. |
| `verifying` | Verifying… | A verification is in progress. |
| `verified` | You're verified | **Verified**. |
| `failed` | Unavailable | **Unavailable**. The next click resets the widget to `idle`; the click after that starts again. |

### Desktop: QR code

On a desktop browser the widget opens a full-page modal with a QR code. The modal names your site by its hostname, without a leading `www.`: "*meridianairlines.com* is asking BotShield to confirm you're human. BotShield never shares your identity — only a signal that you passed." The user scans the code with their phone camera, the BotShield app opens, and they confirm with their device biometric. The modal closes by itself when the result arrives. The modal footer reads "No personal data is shared with this site".

On an [Age Gate](/gate/age-gate) the modal says it is verifying age: the title is "Verify your age with BotShield", and the text reads "*meridianairlines.com* is asking BotShield to verify your age. BotShield never shares your identity — only an age result."

The modal waits up to about five minutes, matching the life of the verification request. If the user selects **Cancel** or clicks outside the modal, the widget returns to `idle` and fires [`botshield:cancel`](#events).

### Mobile: deep link

On a phone or tablet (iPhone, iPad, iPod or Android user agent) there is no QR code. The widget opens the BotShield app directly with a deep link, then checks for the result every 5 seconds for up to 5 minutes. When the user comes back to your tab, the widget picks up where it left off.

### Returning users

Set `platform-user-ref` for signed-in users and two things change after their first successful verification on your site:

* **Recent Presence.** On a gate in Recent Presence mode, a user whose earlier proof is still current passes instantly. No modal opens. The success event has `token: null` and `via: "precheck"`.
* **Push instead of QR.** When a live check is needed and the user has the app with notifications on, BotShield sends the request straight to their phone. The modal title changes to "Check your phone" and reads "Sent to your registered device. Tap the notification to continue — or scan the code if your phone is elsewhere." The QR code stays visible as a fallback.

Live gates and Age Gates never take the instant path, but they still use push.

## Events

All events bubble and cross the Shadow DOM boundary (`bubbles: true, composed: true`), so you can listen on the element, a parent, or `document`.

```html theme={null}
<script>
  const gate = document.querySelector('botshield-verify');

  gate.addEventListener('botshield:success', (e) => {
    const { token, request_id, via } = e.detail;
    // Send token (or request_id when token is null) to your server.
  });

  gate.addEventListener('botshield:failure', (e) => {
    console.warn('BotShield unavailable:', e.detail.reason);
  });
</script>
```

Callbacks and events are equivalent. `onsuccess="myFn"` calls `window.myFn(detail)` right after the `botshield:success` event fires. Use callbacks for plain HTML pages, and events in frameworks and modules where your functions are not global.

### `botshield:success`

Fired when the result is **Verified**. The `detail` has one of two shapes, depending on how the verification ran:

| Path | `detail` |
| - | - |
| Recent Presence fast path | `{ token: null, event_id, request_id, via: "precheck" }` |
| Live check: desktop QR, push, inline passkey or mobile deep link | `{ token, request_id, score: null, via: "ceremony" }` |

| Field | Description |
| - | - |
| `token` | The signed attestation token, a JWT that lives 120 seconds, or `null`. It is always `null` on the fast path, and `null` after a live check in the rare case the completed request carries no token. It is never a request ID. |
| `request_id` | The verification request, `req_…`. Present in both shapes. |
| `event_id` | Same value as `request_id`. Fast path only. |
| `via` | `"precheck"` when no phone step ran, `"ceremony"` when the user confirmed live. |
| `score` | Always `null`. Ignore it. Live check only. |

Handle both shapes:

* If `token` is a JWT, send it to your server with `request_id` and [verify it](/gate/verify-on-your-server) within 120 seconds.
* If `token` is `null`, send `request_id` and have your server read `GET /verification/status` **right away**. A fast-path record reads as `pass` for 60 seconds, then as `expired`. A live check reads as `completed`.
* If `verify-token` rejects a token, treat the user as unverified and fall back to the status check by request ID.

### `botshield:census-status`

Fired once per click, as soon as the pre-check answers and before any modal opens. (`census` is the API's name for BotShield Gate.)

```json theme={null}
{
  "event_id": "req_9f2c4e7a1b3d5f60a1b2c3d4",
  "request_id": "req_9f2c4e7a1b3d5f60a1b2c3d4",
  "verdict": "require_presence",
  "result_state": "unavailable"
}
```

| Field | Values | Description |
| - | - | - |
| `result_state` | `human_verified`, `unavailable` | The result so far. `unavailable` with `verdict: "require_presence"` only means the live check is about to start. |
| `verdict` | `pass`, `require_presence`, `blocked` | What the widget does next: finish as Verified, run the live check, or finish as Unavailable. |

Most integrations do not need this event. Use `botshield:success` and `botshield:failure` for decisions.

### `botshield:failure`

Fired when the result is **Unavailable**. `detail.reason` is always present.

| `reason` | When | Extra `detail` fields |
| - | - | - |
| `origin_not_allowed` | The page origin is not in the site key's allowed origins. This reason means an origin rejection and nothing else. | `message`, `status_code` (403) |
| `invalid_site_key` | The site key does not exist or is revoked. | `message`, `status_code` (403) |
| `gate_not_found` | No gate has this `scope` key in the site key's environment. The gate is placed in the other environment, or the key is misspelled (keys are case-sensitive). | `message`, `status_code` (404) |
| `gate_not_active` | A gate with this `scope` key exists in the site key's environment, but it is a Draft or Archived. Activate it in the Console. | `message`, `status_code` (403) |
| `precheck_rejected` | The pre-check was rejected for another reason, including any other `403`. Read `message`. | `message`, `status_code` |
| `precheck_malformed` | The pre-check answered without a verdict. | `message`, `status_code` |
| `network_error` | The pre-check request could not be sent. | `message`, `status_code` (`null`) |
| `blocked` | BotShield declined this request. | `event_id`, `request_id`, `result_state` (`unavailable`) |
| `modal_create_failed` | The verification request could not be created on desktop. A common cause is an unsupported `mode` value. | — |
| `mobile_create_failed` | The same, on mobile. | — |
| `expired` | The user did not finish within 5 minutes. | — |
| `failed` | The verification ended in a failed state. | — |

For the configuration errors, the widget also writes a plain-English explanation to the browser console. For `gate_not_found`, check the `scope` value and that the gate is placed in the environment your site key belongs to. For `gate_not_active`, activate the gate in the Console.

### `botshield:cancel`

Fired when the person closes the desktop modal before the verification finishes, with the **Cancel** button or a click on the backdrop. `detail` is `{ request_id }`. The widget returns to `idle`, and the next click starts a new verification.

```js theme={null}
gate.addEventListener('botshield:cancel', (e) => {
  console.log('Verification closed by the user:', e.detail.request_id);
});
```

It does not fire on success, failure or expiry; those close the modal and fire their own events. The mobile deep-link flow has no modal, so it never fires this event. There is no callback attribute for it: use `addEventListener`.

### `botshield:checkout`

Fired when the user clicks the [action button](#the-action-button) **and** the widget is verified. `detail` is `{ token, state: "verified" }`. `token` is `null` after a fast-path pass.

### `botshield:inline-passkey`

Fired only with `betas="inline-passkey"`. `detail` is `{ status, reason, request_id }`. See [Inline passkey](#inline-passkey-beta).

### `botshield:reset`

Fired by `reset()`, and when the user clicks a widget in the `failed` state. No `detail`.

<Note>
  `botshield:ready` and `botshield:expired` are not dispatched. An expired request arrives as `botshield:failure` with reason `expired`.
</Note>

## Methods

```js theme={null}
const gate = document.querySelector('botshield-verify');

gate.verify();    // Start a verification, as if the user clicked. Works from idle or failed.
gate.getToken();  // The token from the last success, or null (always null after a fast-path pass).
gate.reset();     // Back to idle, clears the token, fires botshield:reset.
```

Call `reset()` after you consume a result if the user can repeat the action on the same page. Tokens are short-lived, so do not hold one for later.

## The action button

The widget draws a second button under the verify button. It is labelled "Checkout" unless you set `checkout-label`, and it stays disabled until the widget is verified. Because it lives inside the widget, a visitor cannot enable it by editing your page. When a verified user clicks it, you get `botshield:checkout`.

```html theme={null}
<botshield-verify
  id="gate"
  site-key="pk_live_…"
  scope="checkout"
  scan-mode="modal"
  checkout-label="Book flight"
></botshield-verify>

<script>
  let result = null;
  const gate = document.getElementById('gate');

  gate.addEventListener('botshield:success', (e) => { result = e.detail; });

  gate.addEventListener('botshield:checkout', async () => {
    const res = await fetch('/api/bookings', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        flight: 'MA204',
        botshield_token: result.token,
        botshield_request_id: result.request_id,
      }),
    });
    if (!res.ok) gate.reset();
  });
</script>
```

If you have your own submit button, hide the built-in one with `checkout="false"`. The widget then draws only the verify button, and `botshield:checkout` never fires.

```html theme={null}
<botshield-verify
  site-key="pk_live_…"
  scope="checkout"
  scan-mode="modal"
  checkout="false"
></botshield-verify>
```

## Forms

When a live verification succeeds inside a `<form>`, the widget writes the token into a hidden input named `botshield_token` in that form, creating the input if needed. On the fast path there is no token, so nothing is written. Add the request ID yourself to cover both cases:

```html theme={null}
<form id="signup" action="/signup" method="post">
  <input name="email" type="email" required />

  <botshield-verify
    id="gate"
    site-key="pk_live_…"
    scope="signup"
    scan-mode="modal"
    checkout="false"
  ></botshield-verify>

  <input type="hidden" name="botshield_request_id" id="bs-request-id" />
  <button type="submit" id="submit" disabled>Create account</button>
</form>

<script>
  const gate = document.getElementById('gate');
  gate.addEventListener('botshield:success', (e) => {
    document.getElementById('bs-request-id').value = e.detail.request_id;
    document.getElementById('submit').disabled = false;
  });
  gate.addEventListener('botshield:reset', () => {
    document.getElementById('submit').disabled = true;
  });
</script>
```

Your server reads `botshield_token` and `botshield_request_id` from the form post and checks them before it creates the account. Enabling a button in the browser is a convenience, not a control.

## `BotShield.render()`

`window.BotShield.render(target, options)` creates the element for you and returns a handle.

```html theme={null}
<div id="gate-slot"></div>

<script src="https://cdn.botshield.ai/sdk.js"></script>
<script>
  const widget = BotShield.render('#gate-slot', {
    siteKey: 'pk_live_…',
    scanMode: 'modal',
    checkout: false,
    theme: 'dark',
    onSuccess: (detail) => console.log('verified', detail),
    onFailure: (detail) => console.warn('unavailable', detail.reason),
  });

  // render() has no option for these, so set them on the element before the user clicks:
  widget.element.setAttribute('scope', 'checkout');
  widget.element.setAttribute('platform-user-ref', 'usr_48121');
</script>
```

| Option | Attribute it sets |
| - | - |
| `siteKey` (required) | `site-key` |
| `scanMode` | `scan-mode`. Optional; `'modal'` is the default. |
| `checkout` | `checkout`. Pass `false` to hide the [action button](#the-action-button). |
| `theme` | `theme` |
| `onSuccess`, `onFailure` | Listeners for `botshield:success` and `botshield:failure` |

The handle has `verify()`, `getToken()`, `reset()`, `destroy()` (removes the element) and `element`. `target` is a CSS selector or an element; `render()` throws if it is not found.

**Limits.** `render()` cannot set `scope`, `platform-user-ref`, `link-on-verify`, `checkout-label`, `betas` or `mode`. Set them on `widget.element` as shown, or write the element in HTML.

## Inline passkey (Beta)

<Info>
  Beta. This option can change or be removed without a major version. Desktop only.
</Info>

With `betas="inline-passkey"`, the desktop modal first offers a passkey prompt on the same computer, so a user who already has a BotShield passkey available on that device (for example through a synced passkey manager) can confirm with Touch ID, Face ID or Windows Hello without picking up their phone.

```html theme={null}
<botshield-verify
  site-key="pk_live_…"
  scope="checkout"
  scan-mode="modal"
  betas="inline-passkey"
></botshield-verify>
```

What the user sees, in the modal's right-hand pane:

1. "Verify with your passkey — Use Touch ID, Face ID or Windows Hello on this device — no phone needed." with a **Use passkey** button and a **Scan with your phone instead** link.
2. The browser's passkey sheet. On success the pane reads "You're verified" and the modal closes.
3. If the device cannot do it, or the user prefers their phone, the pane switches to the QR code. When the device is capable, the QR view keeps a **Use a passkey on this device instead** link.

The flow never dead-ends: every failure falls back to the QR code. Success arrives as the normal `botshield:success` with `via: "ceremony"`. A new BotShield user enrolls first — a passkey at [app.botshield.ai](https://app.botshield.ai) (public beta) or in the BotShield app — and then the inline path is available to them.

`botshield:inline-passkey` reports progress:

| `status` | Meaning | `reason` |
| - | - | - |
| `ready` | The device has a platform authenticator; the passkey button is live. | `null` |
| `cancelled` | The user dismissed the browser sheet. They can try again. | `null` |
| `retry` | The chosen passkey is not registered with BotShield. The user can pick another. | `credential_not_found` |
| `success` | The passkey check completed. `botshield:success` follows. | `null` |
| `fallback` | The pane switched to the QR code. | See below |

| Fallback `reason` | Meaning |
| - | - |
| `no_webauthn` | The browser has no WebAuthn support, or the page is not a secure context. |
| `no_platform_authenticator` | No built-in biometric authenticator on this device. |
| `capability_check_failed` | The capability check threw. |
| `user_chose_qr` | The user selected **Scan with your phone instead**. |
| `get_failed:<ErrorName>` | The browser's passkey call failed. |
| `no_credential` | The browser returned no credential. |
| `ceremony_failed:<message>` | BotShield could not verify the passkey. |
| `age_gate` | The gate is an [Age Gate](/gate/age-gate). See below. |

**On an Age Gate** the widget skips the inline passkey by itself, because the age signal is read on the user's phone. The modal opens straight on the QR code, with no passkey pane and no link back to one, and the widget dispatches `botshield:inline-passkey` with `{ status: "fallback", reason: "age_gate", request_id }`. You can leave `betas="inline-passkey"` on a page that hosts both gate types.

## Theming

Set `theme` to match your page, or leave `auto`. The widget fills the width of its container; give it a container around 320 px wide for the intended proportions.

Style the exposed parts from your own CSS:

| Part | Element |
| - | - |
| `root` | The wrapper around both buttons |
| `container` | The verify button |
| `branding` | The BotShield lockup on the right of the verify button |
| `checkout` | The action button |

```css theme={null}
botshield-verify::part(container) { border-radius: 12px; }

botshield-verify::part(checkout) {
  background: #0b5fff;
  color: #ffffff;
  border-radius: 12px;
}

/* React to the widget's state */
botshield-verify[state="verified"] { outline: 2px solid #12b76a; outline-offset: 4px; }
```

The QR modal has a fixed dark design and is not themeable.

## Frameworks

Render the element with `site-key` and `scope` already set, and attach listeners with `addEventListener`. Load `sdk.js` once, in your HTML shell.

<Tabs>
  <Tab title="React">
    ```tsx theme={null}
    import { useEffect, useRef } from "react";

    // React 19 typing. On React 18, put the JSX namespace under `declare global` instead.
    declare module "react" {
      namespace JSX {
        interface IntrinsicElements {
          "botshield-verify": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
            "site-key": string;
            scope: string;
            "scan-mode"?: "modal";
            "platform-user-ref"?: string;
            "checkout-label"?: string;
            checkout?: "true" | "false";
            theme?: "light" | "dark" | "auto";
          };
        }
      }
    }

    type SuccessDetail = { token: string | null; request_id: string; via: "precheck" | "ceremony" };

    export function HumanGate({ userRef, onVerified }: { userRef?: string; onVerified: (d: SuccessDetail) => void }) {
      const ref = useRef<HTMLElement>(null);

      useEffect(() => {
        const el = ref.current;
        if (!el) return;
        const onSuccess = (e: Event) => onVerified((e as CustomEvent<SuccessDetail>).detail);
        el.addEventListener("botshield:success", onSuccess);
        return () => el.removeEventListener("botshield:success", onSuccess);
      }, [onVerified]);

      return (
        <botshield-verify
          ref={ref}
          site-key="pk_live_…"
          scope="checkout"
          scan-mode="modal"
          platform-user-ref={userRef}
        />
      );
    }
    ```

    Do not render the element with an empty `site-key` and fill it in later. Wait until you have the key, then render.
  </Tab>

  <Tab title="Vue">
    Tell the compiler that `botshield-verify` is a custom element:

    ```typescript theme={null}
    // vite.config.ts
    import { defineConfig } from "vite";
    import vue from "@vitejs/plugin-vue";

    export default defineConfig({
      plugins: [
        vue({
          template: {
            compilerOptions: { isCustomElement: (tag) => tag === "botshield-verify" },
          },
        }),
      ],
    });
    ```

    Vue's `@` listener syntax works with the event names as they are:

    ```html theme={null}
    <template>
      <botshield-verify
        site-key="pk_live_…"
        scope="checkout"
        scan-mode="modal"
        :platform-user-ref="userRef"
        @botshield:success="onVerified($event.detail)"
        @botshield:failure="onUnavailable($event.detail)"
      />
    </template>
    ```
  </Tab>
</Tabs>

On a single-page app, call `reset()` or re-create the element when the user starts a new action. A verified widget stays verified until then.

## Content Security Policy and origins

Add your page's origin to the site key's **Allowed Origins** in the Console. A live site key must list at least one origin; a test key can have an empty list, which accepts any origin. A mismatch shows up as `botshield:failure` with reason `origin_not_allowed`.

If your site sends a Content Security Policy, allow:

| Directive | Value | Why |
| - | - | - |
| `script-src` | `https://cdn.botshield.ai` | The widget script. |
| `connect-src` | `https://cdn.botshield.ai` | The widget's API calls and status stream. |
| `img-src` | `https://cdn.botshield.ai` `https://api.qrserver.com` | The logo in the modal, and the QR image. |
| `frame-src` | `https://cdn.botshield.ai` | Only for `betas="inline-passkey"`. |
| `style-src` | `'unsafe-inline'` | The widget and its modal inject their own `<style>` blocks. |

The QR image is drawn by a third-party QR service at `api.qrserver.com`. The widget sends it the verification link for the current request, which contains the request ID and no personal data.

For `betas="inline-passkey"`, your `Permissions-Policy` header must not disable `publickey-credentials-get` for `https://cdn.botshield.ai`.

## Next steps

<CardGroup cols={2}>
  <Card title="Verify on your server" icon="server" href="/gate/verify-on-your-server">
    Check the token or request ID before you trust it.
  </Card>

  <Card title="Testing" icon="flask" href="/gate/testing">
    Try every path with development keys.
  </Card>

  <Card title="Age Gate" icon="calendar-check" href="/gate/age-gate">
    Read the age result for an Age Gate.
  </Card>

  <Card title="Result states" icon="circle-check" href="/concepts/result-states">
    Verified and Unavailable, and how to design for both.
  </Card>
</CardGroup>
