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

# Browser SDK

> Integrate the Foil browser SDK: import t.js, call start() with your publishable key, and get a sealed session handoff with getSession() at action time.

Foil's browser surface is intentionally small. Import `t.js`, call `start()`, and call `getSession()` when the user performs a sensitive action.

```js theme={"dark"}
const Foil = await import("https://cdn.usefoil.com/t.js");
const foil = await Foil.start({ publishableKey: "pk_live_..." });

// At action time (signup, login, checkout)
const { sessionId, sealedToken } = await foil.getSession();
```

## Load and initialize

Start Foil as early as possible on your page. The SDK begins collecting signals immediately.

```html theme={"dark"}
<script type="module">
  const foilPromise = import("https://cdn.usefoil.com/t.js")
    .then((Foil) =>
      Foil.start({ publishableKey: "pk_live_your_publishable_key" })
    );

  const foil = await foilPromise;

  // Optional: subscribe to errors
  foil.onError((error) => {
    console.error(error.code, error.message);
  });

  // Optional: pre-warm fingerprinting
  void foil.waitForFingerprint().catch(console.error);
</script>
```

## API reference

### Module exports

| Export           | Type                                                 | Description                                                                     |
| ---------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------- |
| `start(options)` | `(options: FoilStartOptions) => Promise<FoilClient>` | Bootstrap the runtime and start signal collection. Resolves to a `FoilClient`.  |
| `version`        | `string`                                             | SDK bundle version. Useful for support tickets and telemetry.                   |
| `FoilError`      | `class`                                              | Error class thrown by all async methods. See [Error handling](#error-handling). |

### `Foil.start(options)`

```ts theme={"dark"}
interface FoilStartOptions {
  publishableKey: string;   // pk_live_* or pk_test_*
}

function start(options: FoilStartOptions): Promise<FoilClient>;
```

Bootstraps the runtime and begins signal collection. Safe to call on page load.

* **`publishableKey`** is the only option. Pass a `pk_live_*` or `pk_test_*` key. Secret keys (`sk_*`) are rejected.
* `start()` is **idempotent for the same key** — calling it again with the same `publishableKey` resolves to the same client without re-bootstrapping. Calling it with a *different* key rejects with `config.already_initialized`.
* On success, signal collection has started. You don't need to await anything else before calling `getSession()` at action time — the SDK will flush whatever it has collected.
* Rejections are fatal `FoilError`s (see [Error codes](#error-codes)): `config.validation_failed`, `config.already_initialized`, `runtime.bootstrap_failed`, `runtime.integrity_failed`, `runtime.start_failed`, `runtime.start_timeout`.

### `FoilClient`

```ts theme={"dark"}
interface FoilClient {
  getSession(): Promise<SessionHandoff>;
  waitForFingerprint(): Promise<void>;
  onError(handler: (error: FoilErrorPayload) => void): () => void;
  destroy(): void;
}

interface SessionHandoff {
  sessionId: string;     // sid_* — stable across getSession() calls on the same client
  sealedToken: string;   // base64 — opaque, single-use-per-receipt, verify server-side
}
```

#### `getSession()`

Flushes pending observations and returns a sealed handoff for your backend.

* Resolves with `{ sessionId, sealedToken }`. **Every call produces a fresh `sealedToken`**; the `sessionId` stays the same for the lifetime of the client.
* Safe to call multiple times. Calling it twice — e.g. on submit retry — is fine; just send the most recent handoff.
* Concurrent calls are coalesced: if you issue two `getSession()` calls before the first resolves, both receive the same handoff.
* You do **not** need to `await waitForFingerprint()` first. If fingerprinting hasn't resolved, the handoff still works — the verified server-side result just won't include a durable `visitor_fingerprint`.
* Rejects with `runtime.unavailable` (called before `start()` resolved or after `destroy()`) or `runtime.session_failed` (network or server-side rejection, `retryable: true`).

#### `waitForFingerprint()`

Resolves when fingerprinting has completed — useful when you want the handoff to carry a durable visitor ID.

* Optional. No identity data is returned to the browser; the fingerprint ID is only visible server-side in the verified sealed token.
* Typical fingerprint resolution completes within a few hundred milliseconds. Don't block your form submission on it — kick it off after `start()` and use `getSession()` at action time regardless.
* Safe to call in parallel with `getSession()`.
* Rejects with `runtime.fingerprint_failed` (`retryable: true`, `fatal: false`). A subsequent `getSession()` call may still succeed without a fingerprint.

#### `onError(handler)`

Subscribes to `FoilError` events emitted by the runtime.

* Returns an unsubscribe function: `const off = foil.onError(fn); /* later */ off();`.
* If a fatal error has *already* occurred before you attach the handler, it is replayed on the next microtask so you don't miss it.
* The `handler` receives the error as a `FoilErrorPayload` (the `toJSON()` shape of `FoilError`). Exceptions thrown from your handler are swallowed to preserve event-emitter behavior — don't rely on them propagating.
* Use this for logging and observability. For control flow, `await` the promise from `start()` / `getSession()` / `waitForFingerprint()` and catch rejections.

#### `destroy()`

Stops timers and releases resources.

* **Terminal.** After `destroy()` the client cannot be restarted — discard the reference. Any subsequent `getSession()` / `waitForFingerprint()` calls reject with `runtime.unavailable`.
* Useful for SPAs that navigate away from a Foil-protected surface, or for tests that need to tear down between cases.
* Synchronous and best-effort — in-flight network requests may still complete in the background.

## Getting a session handoff

Call `getSession()` right before the protected action — not on page load.

```js theme={"dark"}
async function submitSignup(formData) {
  const { sessionId, sealedToken } = await foil.getSession();

  await fetch("/api/signup", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      ...formData,
      foil: { sessionId, sealedToken },
    }),
  });
}
```

The handoff contains:

```json theme={"dark"}
{
  "sessionId": "sid_7m2k9x4v8q1n5t3w6r0p2c4y8h",
  "sealedToken": "AQAA..."
}
```

Every call produces a fresh handoff. The browser never receives verdicts, scores, or visitor IDs.

## Error handling

All async methods reject with a structured `FoilError`. The same shape is passed to `onError` handlers when the runtime reports a non-thrown error.

```json theme={"dark"}
{
  "code": "config.validation_failed",
  "message": "Foil start options are invalid.",
  "retryable": false,
  "fatal": true,
  "operation": "start",
  "details": {
    "fieldErrors": [{ "field": "publishableKey", "issue": "required" }]
  }
}
```

| Field       | Type                                                 | Description                                                                                         |
| ----------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `code`      | string                                               | Stable, machine-readable identifier. Branch on this — see the [code reference](#error-codes) below. |
| `message`   | string                                               | Human-readable explanation. Safe to log; do not pattern-match.                                      |
| `retryable` | boolean                                              | Whether retrying the failing operation may succeed.                                                 |
| `fatal`     | boolean                                              | Whether the runtime is in a terminal state — further calls will reject with the same error.         |
| `operation` | `'start' \| 'wait_for_fingerprint' \| 'get_session'` | Which method rejected.                                                                              |
| `status`    | number, optional                                     | HTTP status from the Foil API when the cause was a server response.                                 |
| `requestId` | string, optional                                     | Foil request ID. Include verbatim in support tickets.                                               |
| `details`   | object, optional                                     | Code-specific context, e.g. `details.fieldErrors` on validation failures.                           |

### Error codes

| Code                         | Operation              | Retryable | Fatal | When it happens                                                                                                                                           |
| ---------------------------- | ---------------------- | --------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config.validation_failed`   | `start`                | no        | yes   | `publishableKey` is missing, uses the `sk_*` secret prefix, or doesn't match `pk_live_*` / `pk_test_*`. `details.fieldErrors` lists the offending fields. |
| `config.already_initialized` | `start`                | no        | yes   | `start()` was called a second time with a different publishable key in the same page.                                                                     |
| `runtime.bootstrap_failed`   | any                    | no        | yes   | The WASM bundle failed to load or threw during bootstrap. Usually a network or CSP issue — see [Content Security Policy](/content-security-policy).       |
| `runtime.integrity_failed`   | `start`                | no        | yes   | The WASM integrity check failed. The bundle is tampered with or corrupted.                                                                                |
| `runtime.unavailable`        | any                    | no        | yes   | A client method was called before `start()` resolved, or after `destroy()`.                                                                               |
| `runtime.start_failed`       | `start`                | no        | yes   | The runtime rejected during startup. Inspect `cause` for the underlying reason.                                                                           |
| `runtime.start_timeout`      | `start`                | no        | yes   | The runtime didn't signal ready in time. Typically a stalled network request to the Foil API.                                                             |
| `runtime.fingerprint_failed` | `wait_for_fingerprint` | yes       | no    | Fingerprint resolution failed. A subsequent `getSession()` may still succeed without a visitor ID.                                                        |
| `runtime.session_failed`     | `get_session`          | yes       | no    | The session handoff call rejected. Retry once; if it fails again, fall back to your default policy.                                                       |
| `transport.upgrade_required` | any                    | no        | yes   | The Foil API rejected the request with HTTP 426 — the bundle on the page is too old. The user must reload to pick up a fresh `t.js`.                      |

The `code` values above are stable. Categories (`config.*`, `runtime.*`, `transport.*`) are safe to branch on with a wildcard — new codes within a category will keep the same retry/fatal semantics.

### Fallback policy

If Foil fails, your app should degrade gracefully. The pattern below treats any `fatal` error as a skip — you get no signal, but the user can still complete the action.

```js theme={"dark"}
async function getFoilHandoff() {
  try {
    return await foil.getSession();
  } catch (err) {
    if (err.code === 'transport.upgrade_required') {
      // Bundle is stale; reload the page so a fresh t.js loads.
      window.location.reload();
      return null;
    }
    if (err.retryable) {
      try {
        return await foil.getSession();
      } catch {
        // fall through to skip
      }
    }
    // Report to your error tracker and continue without a handoff.
    reportError(err);
    return null;
  }
}

// On the server, a missing handoff means you have no Foil signal for
// this request — fall open or fall closed based on the sensitivity of
// the action. See Going to production for guidance.
```

For server-side error handling and the rest of the `FoilError` envelope as returned by the Foil API itself, see [API errors](/api-reference/errors).

## Best practices

* **Start early** — initialize on page load, not at action time
* **Call `getSession()` late** — right before the sensitive action for the freshest signal data
* **Handle errors gracefully** — if the SDK fails, your app should still work (degrade to a fallback policy)
* **Don't expose verdicts client-side** — the browser API intentionally doesn't return them

## What's next

* [Server verification](/server-verification) — verify the handoff on your backend
* [Browser compatibility](/browser-compatibility) — supported browsers and known differences
* [Quickstart](/quickstart) — end-to-end integration in 5 minutes
