Skip to main content
Foil’s browser surface is intentionally small. Import t.js, call start(), and call getSession() when the user performs a sensitive action.
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.
<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

ExportTypeDescription
start(options)(options: FoilStartOptions) => Promise<FoilClient>Bootstrap the runtime and start signal collection. Resolves to a FoilClient.
versionstringSDK bundle version. Useful for support tickets and telemetry.
FoilErrorclassError class thrown by all async methods. See Error handling.

Foil.start(options)

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 FoilErrors (see Error codes): config.validation_failed, config.already_initialized, runtime.bootstrap_failed, runtime.integrity_failed, runtime.start_failed, runtime.start_timeout.

FoilClient

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.
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:
{
  "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.
{
  "code": "config.validation_failed",
  "message": "Foil start options are invalid.",
  "retryable": false,
  "fatal": true,
  "operation": "start",
  "details": {
    "fieldErrors": [{ "field": "publishableKey", "issue": "required" }]
  }
}
FieldTypeDescription
codestringStable, machine-readable identifier. Branch on this — see the code reference below.
messagestringHuman-readable explanation. Safe to log; do not pattern-match.
retryablebooleanWhether retrying the failing operation may succeed.
fatalbooleanWhether 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.
statusnumber, optionalHTTP status from the Foil API when the cause was a server response.
requestIdstring, optionalFoil request ID. Include verbatim in support tickets.
detailsobject, optionalCode-specific context, e.g. details.fieldErrors on validation failures.

Error codes

CodeOperationRetryableFatalWhen it happens
config.validation_failedstartnoyespublishableKey is missing, uses the sk_* secret prefix, or doesn’t match pk_live_* / pk_test_*. details.fieldErrors lists the offending fields.
config.already_initializedstartnoyesstart() was called a second time with a different publishable key in the same page.
runtime.bootstrap_failedanynoyesThe WASM bundle failed to load or threw during bootstrap. Usually a network or CSP issue — see Content Security Policy.
runtime.integrity_failedstartnoyesThe WASM integrity check failed. The bundle is tampered with or corrupted.
runtime.unavailableanynoyesA client method was called before start() resolved, or after destroy().
runtime.start_failedstartnoyesThe runtime rejected during startup. Inspect cause for the underlying reason.
runtime.start_timeoutstartnoyesThe runtime didn’t signal ready in time. Typically a stalled network request to the Foil API.
runtime.fingerprint_failedwait_for_fingerprintyesnoFingerprint resolution failed. A subsequent getSession() may still succeed without a visitor ID.
runtime.session_failedget_sessionyesnoThe session handoff call rejected. Retry once; if it fails again, fall back to your default policy.
transport.upgrade_requiredanynoyesThe 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.
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.

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