> ## 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.

# Testing

> Use Development keys and gates, the Console Sandbox and a real phone to test every path before you go live.

BotShield Gate has two environments that run side by side on the same API. Build and test in **Development**, then repeat the setup in **Production** when you are ready to launch. A verification always involves a real person and a real phone, so testing means running the flow yourself.

## Development and Production

| | Development | Production |
| - | - | - |
| Site key | `pk_test_…` (Console environment label: **test**) | `pk_live_…` (label: **live**) |
| API key | `bs_dev_…` | `bs_production_…` |
| Billing | Not billed | Metered |
| Activating a gate | No requirements | Verified business, active plan, recent Console passkey confirmation |
| `localhost` and `127.0.0.1` | Always allowed for the site key | Must be listed like any other origin |
| Site key allowed origins | Can be empty; an empty list accepts any origin | At least one origin is required |
| Webhooks | `"environment": "development"` in the payload | `"environment": "production"` in the payload |
| Rate limits | Lower, sized for development | See [Rate limits](/api-reference/rate-limits) |

Rules to remember:

* **A gate's environment is fixed when you place it.** A gate placed in Development never becomes a Production gate. Place a second gate in Production. You can reuse the same key in both environments so only your credentials change. The two gates are independent, each with its own gate type, verification mode and logs.
* **Credentials select the environment.** BotShield resolves a gate in the environment of the key that calls it. A `pk_test_` or `bs_dev_` credential reaches the Active gate with that key in Development, and a `pk_live_` or `bs_production_` credential reaches the one in Production. When that environment has no Active gate with the key, `create-verification-link` returns a `400` "not defined or not active" error, and the widget fires `botshield:failure` with reason `gate_not_found`.
* **The toggle filters the Console.** The **Development | Production** toggle in the page header decides which gates you see and where a new gate is placed.
* **The base URL is the same** for both: `https://api.botshield.ai/operations`, and the same script, `https://cdn.botshield.ai/sdk.js`.

Keep the credentials in configuration so the switch is one change:

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

## The Console Sandbox

Open **BotShield Gate → Sandbox**. The Sandbox runs the real `<botshield-verify>` widget against your own keys and gates, so what you see there is what your page will get.

<Steps>
  <Step title="Pick a site key">
    Choose one of your site keys, or paste one. The panel lists the key's allowed origins. The key's environment decides which gates you can test.
  </Step>

  <Step title="Pick a gate">
    Select **Choose active gate** and pick a gate, or type its key. Only **Active** gates are listed. An Age Gate shows an **Age N+** pill.
  </Step>

  <Step title="Optional: set a Partner User Ref">
    Enter a test value to exercise Recent Presence and push. Keep the same value across clicks: the first click runs the live check, and later clicks on a Recent Presence gate can pass instantly. The **Link on Verify** switch maps to the `link-on-verify` attribute.
  </Step>

  <Step title="Choose Desktop or Mobile">
    Under **Verify link target**, **Desktop** shows the QR modal on the page. **Mobile** shows the QR code on a computer and opens the app directly when you run the Sandbox on a phone.
  </Step>

  <Step title="Click the widget in Live Preview">
    Scan the QR code with your phone and confirm in the BotShield app.
  </Step>
</Steps>

What the Sandbox shows you:

| Panel | Contents |
| - | - |
| **Live Preview** | The widget itself, on a light or dark background. After a success it notes "Token received — verify server-side". |
| **Event Log** | Every widget event in order, with its `detail`. |
| **Integration Code** | An HTML snippet, event-handling code and a server-side validation example for the key and gate you picked. The snippet includes `scan-mode="modal"`. |
| **Response Data** | The pre-check response from the last click, the full verification response, the raw token, its decoded header and payload, and the claims. For an Age Gate it also shows `age_threshold`, `age_verdict` and `age_source`. |

If a test stops with a "pending verification" conflict, select **Revoke Pending & Retry**.

<Note>
  The Sandbox runs on `https://console.botshield.ai`. If your site key has allowed origins, add that origin to the key while you test, or the widget fails with `origin_not_allowed`.
</Note>

## Test on a phone

You need a **BotShield ID**, set up once. The quickest way is [app.botshield.ai](https://app.botshield.ai) (public beta): create a passkey and confirm in the browser, no download. The **BotShield app** for iPhone and Android is optional and in review. The same BotShield ID works for Development and Production gates.

* **Desktop flow.** Open your page on a computer, click the widget and scan the QR code with the phone's camera. Confirm in the app. The modal closes and your page gets `botshield:success`.
* **Mobile flow.** Open your page on the phone itself. The widget opens the app directly; confirm, then switch back to the browser tab. The widget finishes within a few seconds of your return.
* **Reaching a local page from the phone.** A phone cannot load your computer's `localhost`. Serve your test page over HTTPS on a hostname the phone can reach, and add that origin to your test site key.

Then check the server side: send the token to your backend and confirm your [server checks](/gate/verify-on-your-server#what-to-check) pass. Find the request in the gate's **Verification Logs** by its request ID.

## Exercise every path

| Path | How to trigger it | What you should see |
| - | - | - |
| **Verified, live** | Click, scan, confirm. | `botshield:success` with a `token` and `via: "ceremony"`. Status `completed`. |
| **Verified, instant** | On a Recent Presence gate, set `platform-user-ref`, verify once, then click again. | `botshield:success` with `token: null` and `via: "precheck"`. Status reads `pass` for 60 seconds, then `expired`. |
| **Push instead of QR** | With the same `platform-user-ref` and app notifications on, click on a Live gate. | The modal title reads "Check your phone". |
| **Cancelled** | Open the modal and select **Cancel**, or click outside it. | `botshield:cancel` with the `request_id`. The widget returns to `idle`. |
| **Expired** | Open the modal and do not scan. Wait 5 minutes. | `botshield:failure` with reason `expired`. Status `expired`. |
| **Expired, quickly (server-to-server)** | Create a request with a `partner_user_id`, then call `revoke-verification`. | Status `expired` immediately. |
| **Wrong gate key** | Change the case of a letter in the key. | `botshield:failure` with reason `gate_not_found`. |
| **Gate not activated** | Use a Draft or Archived gate's key. | `botshield:failure` with reason `gate_not_active`. |
| **Wrong origin** | Load the widget from an origin that is not on the key's list. | `botshield:failure` with reason `origin_not_allowed`, and a hint in the browser console. |
| **Wrong site key** | Use a revoked site key, or change a character in the key. | `botshield:failure` with reason `invalid_site_key`. |
| **Declined on the phone** | Scan the QR code, then cancel the biometric prompt in the BotShield app. | Your webhook endpoint receives `gate.unavailable`. See [Webhook events](/webhooks/events) for its `reason` values. |
| **Repeat request (server-to-server)** | Create a request with a `partner_user_id`, then create a second one for the same gate before the first finishes. | The second call returns `409`. After the first request completes, fails or is revoked, a new one succeeds. |
| **Age Gate with inline passkey** | Add `betas="inline-passkey"` to an Age Gate's widget and click it on desktop. | The modal opens on the QR code, and `botshield:inline-passkey` fires with `status: "fallback"` and `reason: "age_gate"`. |
| **Expired token** | Hold a token for more than 120 seconds, then verify it. | `valid: false`, reason "Token has expired". |
| **Replay** | Submit the same token or `request_id` twice. | Your server rejects the second one. |
| **Age Unavailable** | On an Age Gate, confirm from a phone whose platform has no age range to share. | `botshield:success` fires, status is `completed`, and `age_verdict` is `unavailable`. Your server must refuse. See [Age Gate](/gate/age-gate). |

Every **Unavailable** path should leave your user with something to do: a retry, or your fallback. See [Result states](/concepts/result-states).

## Pre-launch checklist

<Steps>
  <Step title="Production setup">
    * Business verified and a plan active in the Console.
    * A Console passkey added under **Settings → Profile → Passkeys**.
    * A Production gate placed **and Activated**, with the key your code uses. Keys are case-sensitive.
    * A `pk_live_` site key whose **Allowed Origins** list every production origin, and nothing else. A live key needs at least one origin.
    * A `bs_production_` API key stored as a server secret, if you call the API.
  </Step>

  <Step title="On the page">
    * Every `<botshield-verify>` has the `site-key` and `scope` for Production. `scan-mode="modal"` is the default flow, whether or not the attribute is present.
    * The script loads from `https://cdn.botshield.ai/sdk.js`, and your Content Security Policy [allows it](/gate/web-component#content-security-policy-and-origins).
    * Your code handles both `botshield:success` shapes, including `token: null`.
    * `botshield:failure` leads somewhere useful, `botshield:cancel` leaves the user able to try again, and the built-in action button is either used or hidden with `checkout="false"`.
    * `platform-user-ref` is an opaque ID, not an email address.
  </Step>

  <Step title="On the server">
    * Every protected action checks the result server-side. Nothing is trusted from the browser alone.
    * You check `organization_id`, reject reused `request_id` values, and confirm the gate when you run more than one.
    * You accept `completed` **or** `pass`, and read the status right away for fast-path results.
    * For an Age Gate, you check `age_verdict` or `age_over` with `>=`, and fail closed.
    * With the TypeScript SDK, `serverURL` is set and `apiKeyAuth` includes the `Bearer ` prefix.
  </Step>

  <Step title="Webhooks and timeouts">
    * Your endpoint is added under **Settings → Developer Tools → Webhooks** and verifies signatures. It branches on the payload's top-level `environment`, because one endpoint receives both environments.
    * You time requests out at `expires_at` yourself. No webhook arrives for a request the user never opened, or for a fast-path pass.
  </Step>

  <Step title="One real run">
    Run one Production verification end to end from a real phone, and find it in the gate's **Verification Logs**.
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Place a gate" icon="location-dot" href="/gate/place-a-gate">
    Place and activate the Production gate.
  </Card>

  <Card title="Keys and environments" icon="key" href="/console/keys-and-environments">
    Manage site keys and API keys.
  </Card>

  <Card title="Rate limits" icon="gauge" href="/api-reference/rate-limits">
    Limits per key type.
  </Card>

  <Card title="Webhooks" icon="bell" href="/webhooks/overview">
    Receive results on your server.
  </Card>
</CardGroup>
