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

# Testing your integration

> Test your Foil integration with test keys and simulated bot traffic from Playwright, Puppeteer, or Selenium, then confirm verdicts and enforcement paths.

Before you turn enforcement on in production, confirm two things: your integration **fires on bot traffic** and it **doesn't fire on real users**. This page walks through the patterns we recommend.

## Test vs live keys

Foil issues separate test and live key pairs.

* Test keys are prefixed `pk_test_*` / `sk_test_*`; live keys are `pk_live_*` / `sk_live_*`.
* Sessions created with test keys are tagged in the dashboard so you can filter integration traffic out of real metrics.
* Test and live keys are **not** interchangeable. A sealed token issued under a test publishable key cannot be verified with a live secret key — verification fails locally with a `FoilTokenVerificationError`. Keep the environments fully separate.
* Everything else — verdicts, scoring, phases, fingerprinting — behaves identically across environments.

## What verdicts to expect

An unmodified automation runner should land on `bot` every time. Stealth variants still land on `bot` but with lower confidence, and real user traffic should land on `human`.

| Runner                                                            | Expected `decision.verdict`    | Notes                                                           |
| ----------------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------- |
| Playwright (default Chromium)                                     | `bot`, high `risk_score`       | `webdriver_detected` fires deterministically.                   |
| Puppeteer (default)                                               | `bot`, high `risk_score`       | Same.                                                           |
| Selenium / WebDriver                                              | `bot`, high `risk_score`       | Same.                                                           |
| `playwright-stealth`, `puppeteer-extra-stealth`                   | `bot`, lower `risk_score`      | `attribution.bot.facets.concealment_style` typically `stealth`. |
| Headless Chromium via `--headless=new` with no automation library | `bot`                          | Environment probes catch it; no behavioral signal needed.       |
| Real browser with real user interaction before submit             | `human`, `phase: "behavioral"` | Behavioral signals accumulate once the user moves and types.    |

If you're seeing consistently *different* verdicts than these, something is off in the integration — usually the sealed token isn't reaching the verifier, or `getSession()` is being called against a different `Foil.start()` instance than the one your page initialized.

## Runnable Playwright example

This asserts end-to-end that a bot gets blocked at signup:

```ts theme={"dark"}
import { test, expect, chromium } from '@playwright/test';

test('bot traffic is blocked at signup', async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  await page.goto('http://localhost:3000/signup');
  await page.fill('input[name="email"]', 'bot@example.com');
  await page.fill('input[name="password"]', 'correct-horse-battery-staple');

  const [response] = await Promise.all([
    page.waitForResponse('**/api/signup'),
    page.click('button[type="submit"]'),
  ]);

  expect(response.status()).toBe(403);
  await browser.close();
});
```

Run it with `npx playwright test` against your local dev server. If the assertion passes, your block path is wired correctly. Flip it to `expect(response.status()).toBe(200)` and run against your staging build to verify the allow path for manually-driven sessions.

For Puppeteer, Selenium, or WebDriverIO, the pattern is the same — launch, navigate, submit, assert the response. Foil detects them all.

## Forcing an `inconclusive` verdict

`inconclusive` is Foil saying "not enough signal either way." The easiest way to produce one on purpose is to call `getSession()` immediately on page load, before any user interaction:

```js theme={"dark"}
const foil = await Foil.start({ publishableKey: 'pk_test_...' });
const { sealedToken } = await foil.getSession();
// verify server-side — usually:
//   decision.verdict === "inconclusive"
//   decision.phase === "snapshot"
//   decision.is_provisional === true
```

Use this to test whatever challenge flow you serve on ambiguous sessions — CAPTCHA, email verification, step-up auth. If your integration treats `inconclusive` identically to `human`, you're leaving coverage on the table; see [Verdicts & scoring](/verdicts-and-scoring) for the recommended policy split.

## Debugging common issues

| Symptom                                                                                     | Cause                                                                                   | Fix                                                                                                                                                 |
| ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getSession()` rejects with `runtime.unavailable`                                           | Method called before `start()` resolved, or after `destroy()`                           | Await the `start()` promise before any client method. See [Browser SDK → API reference](/browser-sdk#api-reference).                                |
| `start()` rejects with `runtime.bootstrap_failed`                                           | CSP blocks `cdn.usefoil.com` or `wasm-eval`, or the CDN load fails                      | See [Content Security Policy](/content-security-policy).                                                                                            |
| Sealed token verification fails locally with no clear reason                                | Mismatched environment — test-key token verified with a live secret key (or vice versa) | Confirm `pk_*` and `sk_*` come from the same organization **and** environment.                                                                      |
| Handoff verifies locally but requests to `api.usefoil.com` return `auth.origin_not_allowed` | The publishable key's allowed-origin list doesn't include your dev host                 | Add `http://localhost:3000` (or your dev origin) to the key's allowed origins in the dashboard, or use an unrestricted test key.                    |
| Server receives `sealedToken: undefined`                                                    | Request-body shape mismatch                                                             | Confirm the browser sends JSON with a `foil: { sessionId, sealedToken }` field, and that your body parser runs before your verification middleware. |
| Most *real* test sessions score as `inconclusive`                                           | Page flow calls `getSession()` before any interaction accumulates                       | Call `getSession()` at action time (right before the fetch), not on page load. See [Browser SDK → Best practices](/browser-sdk#best-practices).     |

## What's next

* [Going to production](/going-to-production) — rollout checklist and monitoring
* [Verdicts & scoring](/verdicts-and-scoring) — policy patterns for each verdict band
* [Troubleshooting](/resources/troubleshooting) — production error triage
