---
title: "Documentation — TraceTail"
description: "Add TraceTail with a script tag or npm for React, Next.js, Vue, Angular or Svelte, verify visitor IDs on your server and connect AI agents over MCP."
url: https://tracetail.io/docs
updated: 2026-10-05
---

Documentation

# Recognize returning visitors

TraceTail gives every browser a stable visitor ID with 99.6% accuracy, computed from 40+ browser signals without cookies or local storage. Add one script tag, send the ID to your backend, and recognize the visitor next time.

## Quick start

1. ### Try it without a key

   Paste this before `</body>` on any page, including localhost. Without an API key the SDK computes the visitor ID in the browser and sends nothing, so nothing is recorded in a dashboard.

   **index.html**

   ```html
   <script src="https://tracetail.io/sdk.js"></script>
   <script>
     TraceTail.generateFingerprint().then(({ visitorId }) => {
       // Send visitorId to your backend with the request you want to recognize.
     });
   </script>
   ```

2. ### Add your API key

   Create an account and a key for your site's domain. Every account includes 1,000 identification requests a month, with no card. With `data-api-key` on the tag, each identification is also registered with the API: it counts toward your usage, shows in your dashboard and comes back with a `requestId` and a bot check.

   **index.html**

   ```html
   <script src="https://tracetail.io/sdk.js" data-api-key="YOUR_API_KEY"></script>
   <script>
     TraceTail.generateFingerprint().then(({ visitorId }) => {
       // Send visitorId to your backend with the request you want to recognize.
     });
   </script>
   ```

   [Get an API key](https://tracetail.io/auth)

3. ### Use the visitor ID

   Send `visitorId` to your backend with the request you want to recognize, such as a sign-up, login or checkout, and store it with that event. The same browser gets the same ID on later visits, in normal and incognito windows, and adding a key never changes it.

   To check on your server that an ID really came from TraceTail, send the `requestId` too and verify it with the [Server API](https://tracetail.io/docs#server-api).

   Bundling your JavaScript? [Install](https://tracetail.io/docs#install) covers npm, React, Vue and Angular.

## Keys and domains

An API key is `tt_` followed by 64 hexadecimal characters. Keys are public by design (they ship in your page's HTML), and the domain lock is what keeps them safe: the API accepts a key only from pages on the domain it was created for.

| Key created for | Accepted page origins                                                     |
| --------------- | ------------------------------------------------------------------------- |
| `example.com`   | example.com, www\.example.com and any subdomain, such as shop.example.com |
| `localhost`     | localhost, 127.0.0.1 and \[::1], on any port                              |

Browsers send the page's origin with every request (the `Origin` header, or `Referer` as a fallback), and the API compares it with the key's domain. Requests from any other site, and requests without either header (from a server or curl), are rejected with `401 DOMAIN_MISMATCH`.

Create one key per site in [Settings](https://tracetail.io/settings). For local development, add a key for `localhost`. Your backend uses a different, secret key with the [Server API](https://tracetail.io/docs#server-api).

### Usage and limits

- 1,000 identification requests a month are free, with no card. An account without usage billing stops there: further requests get `429 QUOTA_EXCEEDED` until the 1st of the next month (UTC).
- Add a card in Settings → Billing to lift the cap. Requests beyond the free 1,000 cost $0.10 per 1,000, the same rate at every volume. See [Pricing](https://tracetail.io/pricing).
- Only successful identification requests (`/api/id` and `/api/id/detail`) count. Keyless mode and failed requests are free.
- Rate limits: 100 requests per second per key, and 600 requests a minute from one IP address. Beyond that the API answers `429 RATE_LIMITED` with a `Retry-After` header.

## Install

Use the script tag on any site, or the npm package if you bundle your JavaScript. Both compute the same visitor ID.

**Script tag**

Place the tag before `</body>`. It loads synchronously, so `TraceTail` is ready on the next line.

**index.html**

```html
<script src="https://tracetail.io/sdk.js" data-api-key="YOUR_API_KEY"></script>
<script>
  TraceTail.generateFingerprint().then(({ visitorId }) => {
    // Send visitorId to your backend with the request you want to recognize.
  });
</script>
```

To pin a release, load `https://tracetail.io/sdk/v3.1.2/sdk.js` instead. That file never changes, so you can add an `integrity` attribute. Compute the hash once per release and prefix it with `sha384-`:

**Terminal**

```bash
curl -s https://tracetail.io/sdk/v3.1.2/sdk.js | openssl dgst -sha384 -binary | openssl base64 -A
```

**index.html**

```html
<script
  src="https://tracetail.io/sdk/v3.1.2/sdk.js"
  integrity="sha384-PASTE_THE_HASH_HERE"
  crossorigin="anonymous"
  data-api-key="YOUR_API_KEY"
></script>
```

**npm**

**Terminal**

```bash
npm install @tracetail/js
```

**tracetail.ts**

```typescript
import { TraceTail } from '@tracetail/js';

const tracetail = new TraceTail({ apiKey: 'YOUR_API_KEY', endpoint: 'https://tracetail.io/api' });
const { visitorId } = await tracetail.generateFingerprint();
```

Create one client and reuse it. It keeps the result in memory, so later calls on the same page return at once, and calls made at the same time share one request.

**React**

**Terminal**

```bash
npm install @tracetail/js
```

**useVisitorId.ts**

```typescript
import { useEffect, useState } from 'react';
import { TraceTail } from '@tracetail/js';

const tracetail = new TraceTail({ apiKey: 'YOUR_API_KEY', endpoint: 'https://tracetail.io/api' });

export function useVisitorId() {
  const [visitorId, setVisitorId] = useState<string>();

  useEffect(() => {
    tracetail.generateFingerprint().then((result) => setVisitorId(result.visitorId));
  }, []);

  return visitorId;
}
```

Call `useVisitorId()` in any component. `useEffect` only runs in the browser, so the hook is safe with server-side rendering.

**Vue**

**Terminal**

```bash
npm install @tracetail/js
```

**useVisitorId.ts**

```typescript
import { onMounted, ref } from 'vue';
import { TraceTail } from '@tracetail/js';

const tracetail = new TraceTail({ apiKey: 'YOUR_API_KEY', endpoint: 'https://tracetail.io/api' });

export function useVisitorId() {
  const visitorId = ref<string>();

  onMounted(async () => {
    const result = await tracetail.generateFingerprint();
    visitorId.value = result.visitorId;
  });

  return visitorId;
}
```

Call `useVisitorId()` in `<script setup>`. `onMounted` only runs in the browser, so it is safe with server-side rendering.

**Angular**

**Terminal**

```bash
npm install @tracetail/js
```

**app.component.ts**

```typescript
import { Component, OnInit, signal } from '@angular/core';
import { TraceTail } from '@tracetail/js';

const tracetail = new TraceTail({ apiKey: 'YOUR_API_KEY', endpoint: 'https://tracetail.io/api' });

@Component({
  selector: 'app-root',
  standalone: true,
  template: `<p>Visitor: {{ visitorId() }}</p>`,
})
export class AppComponent implements OnInit {
  readonly visitorId = signal<string | undefined>(undefined);

  async ngOnInit() {
    const result = await tracetail.generateFingerprint();
    this.visitorId.set(result.visitorId);
  }
}
```

With server-side rendering, `ngOnInit` also runs on the server, where there is no browser to identify. Call `generateFingerprint()` from `afterNextRender()` instead.

**Next.js**

**Terminal**

```bash
npm install @tracetail/js
```

**app/use-visitor-id.ts**

```typescript
'use client';

import { useEffect, useState } from 'react';
import { TraceTail } from '@tracetail/js';

const tracetail = new TraceTail({ apiKey: 'YOUR_API_KEY', endpoint: 'https://tracetail.io/api' });

export function useVisitorId() {
  const [visitorId, setVisitorId] = useState<string>();

  useEffect(() => {
    tracetail.generateFingerprint().then((result) => setVisitorId(result.visitorId));
  }, []);

  return visitorId;
}
```

Use the hook from Client Components. The visitor ID is computed in the browser, so it is undefined during server rendering and set after hydration. Send visitorId to your backend with the request you want to recognize (sign-up, login, checkout) and store it with that event.

**Nuxt**

**Terminal**

```bash
npm install @tracetail/js
```

**composables/useVisitorId.ts**

```typescript
import { onMounted, ref } from 'vue';
import { TraceTail } from '@tracetail/js';

const tracetail = new TraceTail({ apiKey: 'YOUR_API_KEY', endpoint: 'https://tracetail.io/api' });

export function useVisitorId() {
  const visitorId = ref<string>();

  onMounted(async () => {
    const result = await tracetail.generateFingerprint();
    visitorId.value = result.visitorId;
  });

  return visitorId;
}
```

Nuxt auto-imports composables, so call useVisitorId() in any component; onMounted only runs in the browser. Send visitorId to your backend with the request you want to recognize (sign-up, login, checkout) and store it with that event.

**Svelte**

**Terminal**

```bash
npm install @tracetail/js
```

**src/lib/VisitorId.svelte**

```html
<script lang="ts">
  import { onMount } from 'svelte';
  import { TraceTail } from '@tracetail/js';

  const tracetail = new TraceTail({ apiKey: 'YOUR_API_KEY', endpoint: 'https://tracetail.io/api' });
  let visitorId = $state<string>();

  onMount(async () => {
    visitorId = (await tracetail.generateFingerprint()).visitorId;
  });
</script>

<p>Visitor: {visitorId}</p>
```

onMount only runs in the browser, so the component is safe with SvelteKit's server rendering. Send visitorId to your backend with the request you want to recognize (sign-up, login, checkout) and store it with that event.

## AI coding agents

TraceTail is built for AI coding agents to add on their own. Keyless mode works without an account, and everything an agent needs is machine-readable: an MCP server, an agent skill, the docs as Markdown and an OpenAPI description. Paste this into Claude Code, Codex, Cursor or any other agent:

**Prompt**

```text
Add TraceTail browser fingerprinting to this app. Read https://tracetail.io/llms.txt (or use the TraceTail MCP server), add the SDK the way this app's framework needs it, get its API keys with a setup link, send the visitor ID to the backend with sign-ups and logins, and verify it there with the Server API.
```

### MCP server

`https://tracetail.io/mcp` is a remote MCP server (Streamable HTTP) that needs no account or key. It works with the 2025 and 2026 versions of the protocol. Add it to your agent:

**Claude Code**

**Terminal**

```bash
claude mcp add --transport http tracetail https://tracetail.io/mcp
```

**Codex**

**Terminal**

```bash
codex mcp add tracetail --url https://tracetail.io/mcp
```

**Cursor**

**.cursor/mcp.json**

```json
{
  "mcpServers": {
    "tracetail": {
      "url": "https://tracetail.io/mcp"
    }
  }
}
```

**VS Code**

**.vscode/mcp.json**

```json
{
  "servers": {
    "tracetail": {
      "type": "http",
      "url": "https://tracetail.io/mcp"
    }
  }
}
```

**Gemini CLI**

**\~/.gemini/settings.json**

```json
{
  "mcpServers": {
    "tracetail": {
      "httpUrl": "https://tracetail.io/mcp"
    }
  }
}
```

**Windsurf**

**mcp\_config.json**

```json
{
  "mcpServers": {
    "tracetail": {
      "serverUrl": "https://tracetail.io/mcp"
    }
  }
}
```

- `get_integration_code`

  tool

  The exact install command and code to add TraceTail browser fingerprinting (a stable visitor ID for each browser, without cookies) to a web app built with the given framework, with where each file goes. Works without an API key (keyless mode, no sign-up); pass the site's public API key to fill it in.

- `search_docs`

  tool

  Searches TraceTail's documentation, pricing, comparison and blog for a topic (e.g. "next.js", "rate limits", "ad blockers", "GDPR", "server-side verification") and returns the matching sections with their URLs.

- `read_docs_page`

  tool

  Returns a page of the TraceTail site as Markdown: "/docs" (the full documentation), "/pricing", "/vs-fingerprintjs", "/" (overview) or a blog post.

- `get_pricing`

  tool

  TraceTail's prices and limits (1,000 identification requests free every month, then $0.10 per 1,000), with the monthly cost at a given volume and the same volume on Fingerprint (FingerprintJS) for comparison.

- `create_setup_link`

  tool

  Gets TraceTail API keys for the person you work for: returns a link to give them, where they sign in with an emailed code and add a card or continue without a card, and a token. Then call check\_setup\_link with its id and token every few seconds: once they've finished, it returns an API key for each site (and a server key if you asked), once.

- `check_setup_link`

  tool

  Where a setup link stands. The first check after the person finishes returns the API keys (and the server key, if requested); they are not shown again. While they're still on the page, check again every few seconds.

It also offers every page of the docs as a resource and an `add-tracetail` prompt. Its card is at `/.well-known/mcp/server-card.json`.

### API keys through a setup link

An agent can get the keys itself. It creates a setup link and gives it to you; you sign in with an emailed code, then add a card (nothing is charged until you go past the free 1,000 requests a month) or continue without a card, and the agent's next check returns an API key for each site, and a server key if it asked for one. Over MCP these are the `create_setup_link` and `check_setup_link` tools; over HTTP:

**Terminal**

```bash
curl -X POST https://tracetail.io/api/v1/setup-links \
  -H "Content-Type: application/json" \
  -d '{"domains": ["example.com", "localhost"], "serverKey": true}'
```

**201 Created**

```json
{
  "id": "sl_5f0c9a…",
  "status": "pending",
  "setupUrl": "https://tracetail.io/setup/sl_5f0c9a…",
  "token": "tt_setup_8e41d2…",
  "next": "Waiting for the person to open the setup link and finish…"
}
```

**Terminal**

```bash
curl https://tracetail.io/api/v1/setup-links/sl_5f0c9a… \
  -H "Authorization: Bearer tt_setup_8e41d2…"
```

**200 OK, once you have finished**

```json
{
  "status": "delivered",
  "account": { "email": "y***@example.com", "paidUsage": true },
  "apiKeys": [
    { "domain": "example.com", "apiKey": "tt_…" },
    { "domain": "localhost", "apiKey": "tt_…" }
  ],
  "serverKey": "tts_…",
  "next": "Put each API key in the TraceTail snippet for its site…"
}
```

The keys come back once, on the first check after you finish; an account that already has a key for a site keeps it, and gets `null` for that site. A server key reads every visitor your account identifies, so a link only creates one for an account it creates: if you sign in to an account you already have, create the server key yourself in [Settings → API keys](https://tracetail.io/settings). Links work for 24 hours, and the token is the agent's secret: the API refuses requests from browsers. URLs such as `https://www.example.com` or `localhost:3000` are reduced to their domain.

### Agent skill

The TraceTail skill teaches an agent the whole integration: the code for each framework, API keys, server-side verification and the limits. Install it with the skills CLI, which reads it from `/.well-known/agent-skills/`:

**Terminal**

```bash
npx skills add https://tracetail.io
```

### Docs for LLMs

- [/llms.txt](https://tracetail.io/llms.txt): what TraceTail is, a quick start and links to everything. [/llms-full.txt](https://tracetail.io/llms-full.txt) holds the documentation in one file.
- Every page has a Markdown copy: add `.md` to its URL (`/docs.md`), or request the page with `Accept: text/markdown`.
- [/openapi.json](https://tracetail.io/openapi.json) describes the API (OpenAPI 3.1), and `/.well-known/api-catalog` lists it (RFC 9727).

## First-party setup

Serve the SDK and the API from a path on your own site, such as `example.com/k3v9/sdk.js`. The browser then only talks to your domain: blockers and filter lists that stop fingerprinting services by their domain never see TraceTail, and your Content Security Policy needs nothing beyond `'self'`.

A small proxy at that path forwards the SDK and identification requests to TraceTail. It sends each visitor's IP address along with a proxy secret, so rate limits and your analytics still see visitors rather than your proxy. Visitor IDs, usage and billing are exactly the same as without it.

### One click on Cloudflare

If your site is on Cloudflare, open [Settings → API keys](https://tracetail.io/settings) and choose **First-party** next to the site's key:

1. Create a Cloudflare API token with the link there. It opens Cloudflare with the three permissions the setup needs: Zone · Zone · Read, Zone · Workers Routes · Edit, Account · Workers Scripts · Edit.
2. Paste the token and deploy. TraceTail uploads a Worker named `tracetail-proxy-example-com` with its own proxy secret, routes `example.com/k3v9/*` and `*.example.com/k3v9/*` to it, and checks that it answers.

The token is used for that one request and never stored, so you can delete it afterwards. Setting up again gives the Worker a new secret, and a new path moves its routes.

### Any other host

Choose **Other hosting** in the same dialog to get a proxy secret, shown once. Keep it on the server as `TRACETAIL_PROXY_SECRET`, never in page HTML. Then run the proxy at your path. For a Worker you deploy yourself, add the variable `TRACETAIL_PATH` and the secret, and route `example.com/k3v9/*` to it. On Next.js 15 or later, add the route handler:

**Cloudflare Worker**

**worker.js**

```javascript
// TraceTail first-party proxy for Cloudflare Workers.
// Serves the TraceTail browser SDK and API from a path on your own site, so the browser only
// talks to your domain. Docs: https://tracetail.io/docs#first-party
//
// Variables: TRACETAIL_PATH, e.g. /k3v9 (route the Worker at yoursite.com/k3v9/*), and
// TRACETAIL_PROXY_SECRET, your proxy secret from TraceTail (add it as a secret).

const UPSTREAM = 'https://tracetail.io';
const FORWARDED_REQUEST_HEADERS = ['accept', 'accept-language', 'content-type', 'origin', 'referer', 'user-agent', 'x-api-key', 'x-tracetail-sdk'];
const FORWARDED_RESPONSE_HEADERS = ['content-type', 'cache-control', 'etag', 'last-modified', 'expires', 'vary', 'x-content-type-options', 'cross-origin-resource-policy', 'retry-after', 'warning', 'x-tracetail-proxy', 'access-control-allow-origin', 'access-control-allow-headers', 'access-control-allow-methods', 'access-control-expose-headers', 'access-control-max-age'];
const FORWARDED_SDK_HEADERS = ['if-none-match', 'if-modified-since'];
const API_PATHS = ['/api/id', '/api/id/detail'];
const MAX_BODY_BYTES = 49152;

export default {
  async fetch(request, env) {
    const base = '/' + String(env.TRACETAIL_PATH || '').replace(/^\/+|\/+$/g, '');
    const url = new URL(request.url);
    if (base === '/' || !url.pathname.startsWith(base + '/')) return notFound();
    const path = url.pathname.slice(base.length);
    const upstream = String(env.TRACETAIL_UPSTREAM || UPSTREAM).replace(/\/+$/, '');

    if (/^\/(?:sdk\/v\d+\.\d+\.\d+\/)?sdk\.js$/.test(path)) {
      if (request.method !== 'GET' && request.method !== 'HEAD') return new Response(null, { status: 405, headers: { Allow: 'GET, HEAD' } });
      // Only what lets the browser revalidate the copy it holds (TraceTail answers 304).
      return passThrough(await fetch(upstream + path, { method: request.method, headers: copied(request, FORWARDED_SDK_HEADERS) }));
    }
    if (API_PATHS.includes(path) || path === '/health') {
      const target = path === '/health' ? '/api/proxy/status' : path;
      const forwarded = await forwardedRequest(request, env);
      if (!forwarded) return Response.json({"error":"Request body is too large.","code":"PAYLOAD_TOO_LARGE"}, { status: 413 });
      return passThrough(await fetch(upstream + target, forwarded));
    }
    return notFound();
  },
};

// Only the headers TraceTail needs: never your site's cookies or other credentials.
// Null when the body is larger than TraceTail accepts.
async function forwardedRequest(request, env) {
  const headers = copied(request, FORWARDED_REQUEST_HEADERS);
  const visitorIp = request.headers.get('cf-connecting-ip');
  if (visitorIp) headers.set('x-tracetail-client-ip', visitorIp);
  if (env.TRACETAIL_PROXY_SECRET) headers.set('x-tracetail-proxy-secret', env.TRACETAIL_PROXY_SECRET);
  const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
  const body = hasBody ? await readBody(request) : undefined;
  return body === null ? null : { method: request.method, headers, body };
}

function copied(request, names) {
  const headers = new Headers();
  for (const name of names) {
    const value = request.headers.get(name);
    if (value) headers.set(name, value);
  }
  return headers;
}

// TraceTail refuses bodies over MAX_BODY_BYTES: stop reading there rather than hold any size in memory.
async function readBody(request) {
  if (Number(request.headers.get('content-length') || 0) > MAX_BODY_BYTES) return null;
  if (!request.body) return undefined;
  const reader = request.body.getReader();
  const chunks = [];
  let size = 0;
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    size += value.byteLength;
    if (size > MAX_BODY_BYTES) {
      await reader.cancel();
      return null;
    }
    chunks.push(value);
  }
  const body = new Uint8Array(size);
  let offset = 0;
  for (const chunk of chunks) {
    body.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return body.buffer;
}

// Only the headers about this response: TraceTail's site-wide ones (HSTS, CSP) would apply to your site.
function passThrough(response) {
  const headers = new Headers();
  for (const name of FORWARDED_RESPONSE_HEADERS) {
    const value = response.headers.get(name);
    if (value) headers.set(name, value);
  }
  return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
}

function notFound() {
  return new Response('Not found', { status: 404 });
}
```

**Next.js**

**app/k3v9/\[...route]/route.ts**

```typescript
// app/k3v9/[...route]/route.ts — TraceTail first-party proxy for Next.js 15+ (App Router).
// Serves the TraceTail SDK and API from /k3v9 on your own site. Docs: https://tracetail.io/docs#first-party
// Set TRACETAIL_PROXY_SECRET (your proxy secret from TraceTail) in your environment.

const UPSTREAM = 'https://tracetail.io';
const FORWARDED_REQUEST_HEADERS = ['accept', 'accept-language', 'content-type', 'origin', 'referer', 'user-agent', 'x-api-key', 'x-tracetail-sdk'];
const FORWARDED_RESPONSE_HEADERS = ['content-type', 'cache-control', 'etag', 'last-modified', 'expires', 'vary', 'x-content-type-options', 'cross-origin-resource-policy', 'retry-after', 'warning', 'x-tracetail-proxy', 'access-control-allow-origin', 'access-control-allow-headers', 'access-control-allow-methods', 'access-control-expose-headers', 'access-control-max-age'];
const FORWARDED_SDK_HEADERS = ['if-none-match', 'if-modified-since'];
const API_PATHS = ['/api/id', '/api/id/detail'];
const MAX_BODY_BYTES = 49152;

async function proxy(request: Request, context: { params: Promise<{ route: string[] }> }): Promise<Response> {
  const path = '/' + (await context.params).route.join('/');
  const isSdk = /^\/(?:sdk\/v\d+\.\d+\.\d+\/)?sdk\.js$/.test(path);
  let target: string;
  if (isSdk) target = path;
  else if (API_PATHS.includes(path)) target = path;
  else if (path === '/health') target = '/api/proxy/status';
  else return new Response('Not found', { status: 404 });

  // Only the headers TraceTail needs: never your site's cookies or other credentials. For the SDK,
  // only what lets the browser revalidate the copy it holds (TraceTail answers 304).
  const headers = new Headers();
  for (const name of isSdk ? FORWARDED_SDK_HEADERS : FORWARDED_REQUEST_HEADERS) {
    const value = request.headers.get(name);
    if (value) headers.set(name, value);
  }
  if (isSdk) return passThrough(await fetch(UPSTREAM + target, { method: request.method, headers, cache: 'no-store' }));
  // X-Real-IP as your host sets it (Vercel does; behind nginx, set it to $remote_addr). Otherwise the
  // address your nearest proxy appended to X-Forwarded-For: the entries before it come from the visitor.
  const visitorIp = request.headers.get('x-real-ip') ?? request.headers.get('x-forwarded-for')?.split(',').pop()?.trim();
  if (visitorIp) headers.set('x-tracetail-client-ip', visitorIp);
  if (process.env.TRACETAIL_PROXY_SECRET) headers.set('x-tracetail-proxy-secret', process.env.TRACETAIL_PROXY_SECRET);

  const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
  const body = hasBody ? await readBody(request) : undefined;
  if (body === null) return Response.json({"error":"Request body is too large.","code":"PAYLOAD_TOO_LARGE"}, { status: 413 });
  return passThrough(await fetch(UPSTREAM + target, { method: request.method, headers, body, cache: 'no-store' }));
}

// Only the headers about this response: TraceTail's site-wide ones (HSTS, CSP) would apply to your site.
function passThrough(upstream: Response): Response {
  const responseHeaders = new Headers();
  for (const name of FORWARDED_RESPONSE_HEADERS) {
    const value = upstream.headers.get(name);
    if (value) responseHeaders.set(name, value);
  }
  return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: responseHeaders });
}

// TraceTail refuses bodies over MAX_BODY_BYTES: stop reading there rather than hold any size in memory.
async function readBody(request: Request): Promise<ArrayBuffer | null | undefined> {
  if (Number(request.headers.get('content-length') || 0) > MAX_BODY_BYTES) return null;
  if (!request.body) return undefined;
  const reader = request.body.getReader();
  const chunks: Uint8Array[] = [];
  let size = 0;
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    size += value.byteLength;
    if (size > MAX_BODY_BYTES) {
      await reader.cancel();
      return null;
    }
    chunks.push(value);
  }
  const body = new Uint8Array(size);
  let offset = 0;
  for (const chunk of chunks) {
    body.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return body.buffer;
}

export const GET = proxy;
export const HEAD = proxy;
export const POST = proxy;
```

Anything else that forwards HTTP requests works too, such as nginx, Express or a serverless function. Forward these:

| On your site                                  | Forward to                                                                         |
| --------------------------------------------- | ---------------------------------------------------------------------------------- |
| `GET /k3v9/sdk.js`                            | `https://tracetail.io/sdk.js`, and `/k3v9/sdk/v3.1.2/sdk.js` to the pinned release |
| `POST /k3v9/api/id` and `/k3v9/api/id/detail` | `https://tracetail.io/api/id` and `/api/id/detail`, with the body unchanged        |
| `GET /k3v9/health`                            | `https://tracetail.io/api/proxy/status`                                            |

Forward only these request headers: `Accept`, `Accept-Language`, `Content-Type`, `Origin`, `Referer`, `User-Agent`, `X-API-Key`, `X-TraceTail-SDK`, never cookies or `Authorization`. Add `X-TraceTail-Client-IP` with the visitor's IP address, as your own server sees it rather than from a header the visitor could set, and `X-TraceTail-Proxy-Secret` with your proxy secret.

Pass back only these response headers: `Content-Type`, `Cache-Control`, `ETag`, `Last-Modified`, `Expires`, `Vary`, `X-Content-Type-Options`, `Cross-Origin-Resource-Policy`, `Retry-After`, `Warning`, `X-TraceTail-Proxy`, `Access-Control-Allow-Origin`, `Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Access-Control-Expose-Headers`, `Access-Control-Max-Age`. TraceTail's others are set for tracetail.io as a whole: on your domain, its `Strict-Transport-Security` would force HTTPS on every subdomain for a year, and its `Content-Security-Policy` isn't yours.

### Load the SDK through the proxy

Point the tag at your path. `data-endpoint` sends the identification requests through the proxy too:

**index.html**

```html
<script src="/k3v9/sdk.js" data-api-key="YOUR_API_KEY" data-endpoint="/k3v9/api"></script>
<script>
  TraceTail.generateFingerprint().then(({ visitorId }) => {
    // Send visitorId to your backend with the request you want to recognize.
  });
</script>
```

With npm, set `endpoint`:

**tracetail.ts**

```typescript
import { TraceTail } from '@tracetail/js';

const tracetail = new TraceTail({ apiKey: 'YOUR_API_KEY', endpoint: '/k3v9/api' });
const { visitorId } = await tracetail.generateFingerprint();
```

### Check it

Open the health check on your site. `trusted` means TraceTail accepts the proxy's secret, and `visitorIp` should be your own IP address:

```http
GET https://example.com/k3v9/health

{ "proxy": "trusted", "domain": "example.com", "visitorIp": "203.0.113.7" }
```

`untrusted` with the reason `no-secret` means the proxy isn't sending the secret, and `unknown-secret` that the secret isn't current: set the proxy up again. Identification responses through a working proxy carry `X-TraceTail-Proxy: trusted`, and **Check the proxy** in Settings runs the same test from TraceTail.

Pick a path that doesn't look like analytics, because filter lists match words such as `track`, `analytics` and `collect` in URLs. The dashboard suggests a random one.

## SDK reference

The script tag and the npm package have the same methods and return the same result.

### Script tag: window\.TraceTail

The script configures itself from its own tag: `data-api-key` (leave it out for keyless mode) and `data-endpoint` (defaults to `https://tracetail.io/api`). Loading it collects and sends nothing; the SDK starts when you call `generateFingerprint()`.

- `generateFingerprint(options?)`

  Promise\<Result>

  Identifies this browser. See the options and the result below.

- `getFingerprint(options?)`

  Promise\<Result>

  Same as `generateFingerprint`.

- `configure(options)`

  void

  Changes options at runtime, merged into the current ones; `configure({ debug: true })` keeps the key from the tag. It also clears the cached result.

- `getComplianceContext()`

  ComplianceContext

  Privacy and region hints from the browser; see [Privacy and compliance](https://tracetail.io/docs#privacy).

- `version`

  string

  The SDK version, currently `3.1.2`.

Using TypeScript with the script tag? Type declarations for the global are at `https://tracetail.io/sdk/tracetail.d.ts`.

### npm: new TraceTail(options)

Import it with `import { TraceTail } from '@tracetail/js'`. An instance has `generateFingerprint(options?)`, `getComplianceContext()` and `version`. `getComplianceContext` is also a named export, for use without an instance. Importing the package and creating the client never touch the browser, so both are safe during server-side rendering.

### Options

For `new TraceTail(options)` and `TraceTail.configure(options)`:

- `apiKey`

  string

  Your API key. Leave it out for keyless mode.

- `endpoint`

  string

  The API base URL. Default: `https://tracetail.io/api`

- `timeout`

  number

  How long to wait for the API, in milliseconds. A timeout sets `error.code` to `TIMEOUT`. Default: `5000`

- `debug`

  boolean

  Logs each step and any API error to the console. Default: `false`

### generateFingerprint(options?)

- `verbose`

  boolean

  Adds the collected signals to the result as `components`.

- `detailed`

  boolean

  Calls `/api/id/detail` instead of `/api/id`; the result adds `riskAssessment` and `botDetection`. Needs a key; keyless mode ignores it.

- `refresh`

  boolean

  Skips the in-memory cache and identifies again.

### Result

- `visitorId`

  string

  `fp3_` followed by 16 hex characters. The same in keyless and keyed mode.

- `quality`

  { score, reasons }

  Signal completeness from 0 to 1, with reasons for missing or reduced information. This is not match probability.

- `confidence`

  number

  Deprecated alias of quality.score. Not an accuracy estimate.

- `verification`

  string

  In keyed mode: consistent when the server recomputed the ID from the signals; unverified for legacy requests. This does not authenticate a device.

- `processingTime`

  number

  Milliseconds from the call to the result, measured in the browser, including the API call in keyed mode.

- `requestId`

  string?

  Keyed mode: the ID of the registered request, which your server can verify with the [Server API](https://tracetail.io/docs#server-api).

- `isBot`

  boolean?

  Keyed mode: true for automated browsers (headless Chrome, WebDriver).

- `riskAssessment, botDetection`

  object?

  With `detailed: true`. See [REST API](https://tracetail.io/docs#rest-api).

- `components`

  object?

  With `verbose: true`: the signals behind the ID (`basic`, `hardware`, `canvas`, `webgl`, `fonts`), plus `automation`, which is not part of the ID.

- `error`

  { status, code, message }?

  Keyed mode, when the API call failed. The visitor ID is still returned.

### Handling errors

In keyed mode, `generateFingerprint()` doesn't reject when the API call fails. It resolves with the visitor ID, computed in the browser, and an `error`, so your page keeps working. Failed results aren't cached: the next call tries the API again.

```javascript
const result = await TraceTail.generateFingerprint();

if (result.error) {
  // For example 401 DOMAIN_MISMATCH, 429 RATE_LIMITED, or 0 NETWORK_ERROR
  console.warn('TraceTail:', result.error.status, result.error.code, result.error.message);
}
```

`error.status` is the HTTP status, or `0` when the request never completed. `error.code` is one of the API codes under [Errors](https://tracetail.io/docs#rest-api-errors), or one set by the SDK: `NETWORK_ERROR` and `TIMEOUT` (status 0), `HTTP_ERROR` (an error response without a code) and `INVALID_RESPONSE` (a success response the SDK couldn't read). `generateFingerprint()` only rejects when it can't run at all, for example on a server during rendering.

### Debugging

Call `TraceTail.configure({ debug: true })`, or pass `debug: true` to `new TraceTail()`. The SDK then logs each step and any API error to the console.

## REST API

The SDK makes these calls for you. They're documented so you can set up your Content Security Policy and read the requests in your browser's network panel. Every call must come from a page on the key's domain.

### Identify: POST /api/id

**Request**

```http
POST https://tracetail.io/api/id
Content-Type: application/json
X-API-Key: YOUR_API_KEY
X-TraceTail-SDK: js/3.1.2
Origin: https://www.example.com

{
  "visitorId": "fp3_e8fc80d60f8c3571",
  "components": {
    "basic": {
      "browser": "chrome",
      "os": "macos",
      "language": "en-US",
      "platform": "MacIntel",
      "cookieEnabled": true,
      "screen": [
        1440,
        900
      ],
      "colorDepth": 30,
      "timezone": "Europe/Berlin"
    },
    "hardware": {
      "hardwareConcurrency": 8,
      "maxTouchPoints": 0,
      "deviceMemory": 8
    },
    "canvas": {
      "hash": "example",
      "samples": [
        12480,
        9216,
        14111,
        7340
      ]
    },
    "webgl": {
      "vendor": "WebKit",
      "renderer": "WebKit WebGL",
      "version": "WebGL 1.0",
      "unmaskedVendor": "Apple Inc.",
      "unmaskedRenderer": "Apple GPU"
    },
    "fonts": {
      "detected": [
        "Arial",
        "Helvetica"
      ],
      "count": 2,
      "hash": "example"
    },
    "automation": {
      "webdriver": false
    }
  },
  "confidence": 1
}
```

The body carries the visitor ID computed in the browser, the collected signals (at most 32 KB) and a legacy confidence field. The server validates the complete v3 signals, recomputes the ID and signal quality, and records the request before returning success. The browser adds the `Origin` header, which the API checks against the key's domain. A successful response:

**200 OK**

```json
{
  "requestId": "3f8c2a1e-6b4d-4c7a-9e21-5d0f7b8a9c13",
  "visitorId": "fp3_e8fc80d60f8c3571",
  "confidence": 1,
  "quality": { "score": 1, "reasons": [] },
  "verification": "consistent",
  "isBot": false
}
```

### Detailed: POST /api/id/detail

Same request. The response adds `riskAssessment` and `botDetection`; the SDK calls this endpoint when you pass `detailed: true`.

**200 OK**

```json
{
  "requestId": "3f8c2a1e-6b4d-4c7a-9e21-5d0f7b8a9c13",
  "visitorId": "fp3_e8fc80d60f8c3571",
  "confidence": 1,
  "quality": { "score": 1, "reasons": [] },
  "verification": "consistent",
  "isBot": false,
  "riskAssessment": { "level": "low", "score": 0, "signals": [] },
  "botDetection": { "isBot": false, "verdict": "not_detected", "automationTools": [] }
}
```

- `riskAssessment`

  { level, score, signals }

  `level` is `low`, `medium` or `high`; `score` runs from 0 to 1; `signals` lists what was found as `{ type, detail }`. Types: `headless_browser`, `webdriver`, `user_agent_mismatch`, `platform_mismatch`, `software_renderer`, `virtual_machine`, `insufficient_signals`.

- `botDetection`

  { isBot, automationTools }

  `isBot` is true only on direct evidence: a headless browser user agent or `navigator.webdriver`. `automationTools` names what was detected.

### Errors

Errors come back as JSON with a code. The SDK hands them to you as `result.error`.

**401 Unauthorized**

```json
{
  "error": "This API key is for example.com; the request came from localhost.",
  "code": "DOMAIN_MISMATCH"
}
```

| Status | Code                    | Meaning                                                                                                                           |
| ------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `VALIDATION_ERROR`      | The body is not valid JSON or lacks a valid ID and complete v3 components.                                                        |
| 400    | `ID_COMPONENT_MISMATCH` | The ID does not match its signals. Generate a fresh SDK result.                                                                   |
| 401    | `INVALID_KEY`           | The key does not exist or was deactivated.                                                                                        |
| 409    | `IDEMPOTENCY_CONFLICT`  | A retry token was reused with different data. Generate a fresh SDK result.                                                        |
| 401    | `DOMAIN_MISMATCH`       | The page is not on the key’s domain, or the request had no Origin or Referer.                                                     |
| 402    | `PAYMENT_REQUIRED`      | Past the free 1,000 this month with an unpaid or canceled subscription. Update it in Settings → Billing.                          |
| 413    | `PAYLOAD_TOO_LARGE`     | The signals are larger than 32 KB.                                                                                                |
| 429    | `QUOTA_EXCEEDED`        | An account without usage billing used its 1,000 requests this month. Add a card in Settings → Billing, or wait for the 1st (UTC). |
| 429    | `RATE_LIMITED`          | More than 100 requests a second for the key, or 600 a minute from one IP. Retry after the number of seconds in `Retry-After`.     |
| 503    | `DB_UNAVAILABLE`        | A temporary outage. Retry later.                                                                                                  |

### Status

`GET https://tracetail.io/api/health` reports whether the API is up, and the [status page](https://tracetail.io/status) shows current incidents.

## Server API

Have the page send its `requestId` with the visitor ID, then look it up from your backend. The Server API returns what TraceTail recorded for that request, from your account only. Require `verification: consistent`and check its domain and age. This confirms the ID matches the submitted signals; client signals can still be fabricated, so a fingerprint is not authentication or proof of a human.

Bind the action to your application's session and store the request ID with a unique constraint in the same transaction as the action. This prevents replay; reading an identification does not consume it.

**In the page**

```javascript
const { visitorId, requestId } = await TraceTail.generateFingerprint();

await fetch('/signup', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, visitorId, requestId }),
});
```

### Server keys

The Server API takes a server key: `tts_` followed by 64 hexadecimal characters. Create one in [Settings → API keys](https://tracetail.io/settings), under Server API keys. It's shown once, and TraceTail stores only a hash. Unlike your public key, a server key is a secret that reads your whole account, so keep it on your server, for example in a `TRACETAIL_SERVER_KEY` environment variable. An account can have 5 server keys, and a revoked key stops working within a minute.

Send the key in the `Authorization` header as `Bearer tts_…`. A key in the URL or in another header is refused, and so is any request with an `Origin` header: browsers send one, and the Server API sends no CORS headers.

### Verify an identification: GET /api/v1/identifications/:requestId

**Terminal**

```bash
curl https://tracetail.io/api/v1/identifications/3f8c2a1e-6b4d-4c7a-9e21-5d0f7b8a9c13 \
  -H "Authorization: Bearer $TRACETAIL_SERVER_KEY"
```

**200 OK**

```json
{
  "requestId": "3f8c2a1e-6b4d-4c7a-9e21-5d0f7b8a9c13",
  "visitorId": "fp3_e8fc80d60f8c3571",
  "domain": "www.example.com",
  "endpoint": "/api/id",
  "createdAt": "2026-10-05T09:12:44.000Z",
  "ipAddress": "203.0.113.7",
  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
  "browser": "Chrome",
  "os": "macOS",
  "confidence": 1,
  "riskScore": 0,
  "quality": { "score": 1, "reasons": [] },
  "verification": "consistent",
  "isBot": false
}
```

- `requestId`

  string

  The ID the identification returned to the browser.

- `visitorId`

  string

  The visitor ID the browser sent with it. Compare it with the one your page sent you.

- `domain`

  string

  The host of the page that made the request, such as `www.example.com`.

- `endpoint`

  string

  `/api/id` or `/api/id/detail`.

- `createdAt`

  string

  When TraceTail registered the identification, in ISO 8601 (UTC).

- `ipAddress`

  string | null

  The visitor's IP address; through a [first-party proxy](https://tracetail.io/docs#first-party), the one it forwarded.

- `userAgent`

  string | null

  The browser's `User-Agent` header.

- `browser, os`

  string | null

  Such as `Chrome` and `macOS`, from this request's User-Agent header.

- `confidence, riskScore`

  number | null

  From 0 to 1, captured for this request: signal completeness (not match probability) and the bot risk score (`riskAssessment.score` on [/api/id/detail](https://tracetail.io/docs#rest-api)). Historical events without an assessment return null.

Check that the identification is for the visitor ID your page sent, that it's recent and that it came from your site. A `404` means the requestId is not a successful identification in your account.

**Node.js**

**verify.js**

```javascript
const TEN_MINUTES_MS = 10 * 60 * 1000;

// requestId and visitorId as your page sent them.
async function verifyIdentification(requestId, visitorId) {
  const response = await fetch(
    `https://tracetail.io/api/v1/identifications/${encodeURIComponent(requestId)}`,
    { headers: { Authorization: `Bearer ${process.env.TRACETAIL_SERVER_KEY}` } },
  );
  if (response.status === 404) return false; // not an identification in your account
  if (!response.ok) throw new Error(`TraceTail Server API: HTTP ${response.status}`);

  const identification = await response.json();
  return identification.verification === 'consistent'
    && identification.visitorId === visitorId
    && Date.now() >= Date.parse(identification.createdAt)
    && Date.now() - Date.parse(identification.createdAt) < TEN_MINUTES_MS
    && (identification.domain === 'example.com' || identification.domain.endsWith('.example.com'));
}
```

**Python**

**verify.py**

```python
import os
from datetime import datetime, timedelta, timezone
from urllib.parse import quote

import requests

# request_id and visitor_id as your page sent them.
def verify_identification(request_id, visitor_id):
    response = requests.get(
        f"https://tracetail.io/api/v1/identifications/{quote(request_id, safe='')}",
        headers={"Authorization": f"Bearer {os.environ['TRACETAIL_SERVER_KEY']}"},
        timeout=5,
    )
    if response.status_code == 404:
        return False  # not an identification in your account
    response.raise_for_status()

    identification = response.json()
    created_at = datetime.fromisoformat(identification["createdAt"].replace("Z", "+00:00"))
    domain = identification["domain"]
    return (
        identification["verification"] == "consistent"
        and identification["visitorId"] == visitor_id
        and created_at <= datetime.now(timezone.utc)
        and datetime.now(timezone.utc) - created_at < timedelta(minutes=10)
        and (domain == "example.com" or domain.endswith(".example.com"))
    )
```

### Look up a visitor: GET /api/v1/visitors/:visitorId

What your account knows about one visitor, for example to see whether the browser behind a sign-up has been here before:

**Request**

```http
GET https://tracetail.io/api/v1/visitors/fp3_e8fc80d60f8c3571
Authorization: Bearer tts_…
```

**200 OK**

```json
{
  "visitorId": "fp3_e8fc80d60f8c3571",
  "firstSeenAt": "2026-08-14T17:03:10.000Z",
  "lastSeenAt": "2026-10-05T09:12:44.000Z",
  "visits": 12,
  "browser": "Chrome",
  "os": "macOS",
  "confidence": 1,
  "riskScore": 0,
  "recentIdentifications": [
    {
      "requestId": "3f8c2a1e-6b4d-4c7a-9e21-5d0f7b8a9c13",
      "domain": "www.example.com",
      "endpoint": "/api/id",
      "createdAt": "2026-10-05T09:12:44.000Z",
      "ipAddress": "203.0.113.7",
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
    }
  ]
}
```

- `visitorId`

  string

  The visitor ID you looked up.

- `firstSeenAt`

  string

  When TraceTail first identified the visitor for your account (ISO 8601).

- `lastSeenAt`

  string

  Their latest identification (ISO 8601).

- `visits`

  number

  Successful identifications of the visitor in the last 90 days.

- `browser, os`

  string | null

  Signal completeness and bot risk from the latest visit. Older visits may use legacy scoring.

- `confidence, riskScore`

  number | null

  Signal completeness and bot risk from the latest visit. Older visits may use legacy scoring.

- `recentIdentifications`

  object\[]

  The latest 20, newest first, each with `requestId`, `domain`, `endpoint`, `createdAt`, `ipAddress` and `userAgent`.

### Errors

Errors are JSON with a code, like the identification API's.

| Status | Code                 | Meaning                                                                                                        |
| ------ | -------------------- | -------------------------------------------------------------------------------------------------------------- |
| 400    | `VALIDATION_ERROR`   | The `requestId` is not a UUID, or the visitor ID is not one the SDK computes.                                  |
| 401    | `INVALID_SERVER_KEY` | No server key in the `Authorization` header, a key in the URL or another header, or an unknown or revoked key. |
| 403    | `BROWSER_REQUEST`    | The request has an `Origin` header, so it came from a browser. Call the Server API from your server.           |
| 404    | `NOT_FOUND`          | No successful identification, or no visitor, with that ID in your account. Refused requests have no requestId. |
| 429    | `RATE_LIMITED`       | More than 600 requests a minute with the key. Retry after the number of seconds in `Retry-After`.              |
| 503    | `DB_UNAVAILABLE`     | A temporary outage. Retry later.                                                                               |

### Limits and retention

A server key can make 600 requests a minute, and Server API requests don't count toward your usage. Identifications can be looked up for 90 days, as long as request logs are kept, and a visitor until 180 days after their last visit. Erasing a visitor (see [Privacy and compliance](https://tracetail.io/docs#privacy-erasure)) removes both.

## Privacy and compliance

### What TraceTail stores

Keyless mode sends nothing. For keyed requests, TraceTail stores the visitor ID with the collected signals, the request's IP address and its user agent. A visitor's record is deleted after 180 days without a visit, and API request logs after 90 days.

### Erasing a visitor

When a visitor asks you to delete their data, open [Settings → Privacy](https://tracetail.io/settings?tab=privacy), enter their visitor ID and confirm. TraceTail deletes the visitor's record (signals, IP address and user agent) and every API log carrying that visitor ID, within your account only.

The same erasure is available as `DELETE /api/visitors/:visitorId` for a session signed in to your dashboard, for example from the browser console on tracetail.io. It returns `400` for an invalid ID and `401` when you're signed out.

```javascript
const response = await fetch('/api/visitors/fp3_e8fc80d60f8c3571', { method: 'DELETE' });
console.log(await response.json());
// { success: true, visitorId: 'fp3_e8fc80d60f8c3571', deleted: { fingerprints: 1, apiCallLogs: 12 } }
```

### Consent

You are responsible for any consent the law requires where your visitors are, for example under the ePrivacy Directive and GDPR in the EU or PECR in the UK. TraceTail does not show a consent banner. Because the script collects nothing until you call `generateFingerprint()`, you can wait for consent before calling it:

```javascript
const { likelyRequiresConsent } = TraceTail.getComplianceContext();

// hasFingerprintingConsent() stands for your consent manager.
if (!likelyRequiresConsent || hasFingerprintingConsent()) {
  TraceTail.generateFingerprint().then(({ visitorId }) => {
    // Send visitorId to your backend.
  });
}
```

### getComplianceContext()

Hints worked out in the browser. They are not geolocation, and they never change what the SDK collects.

- `gpcEnabled`

  boolean

  Global Privacy Control is on.

- `doNotTrack`

  boolean

  Do Not Track is on.

- `timezone`

  string

  The IANA time zone, such as Europe/Berlin.

- `locale`

  string

  The browser language, such as de-DE.

- `region`

  'EU' | 'EEA' | 'UK' | 'US' | 'OTHER'

  Inferred from the time zone, then the locale.

- `isEU, isEEA, isUK`

  boolean

  Shorthands for `region`; the EEA includes the EU.

- `likelyRequiresConsent`

  boolean

  True when `isEEA`, `isUK`, `gpcEnabled` or `doNotTrack` is true.

## Troubleshooting

### The Content Security Policy blocks the SDK

Allow the script and the API in your policy:

```text
Content-Security-Policy: script-src 'self' https://tracetail.io; connect-src 'self' https://tracetail.io
```

Keyless pages only need `script-src`. With the npm package the SDK is in your bundle, so you only need `connect-src`, and only with a key. With a [first-party setup](https://tracetail.io/docs#first-party), `'self'` covers both.

### 401 DOMAIN\_MISMATCH

The page's origin doesn't match the key's domain. Check the domain in Settings: `www.` and subdomains are covered, other domains are not. Use a `localhost` key for local development. Calls from servers, curl or scripts have no page origin and always get this error; from a server, use the [Server API](https://tracetail.io/docs#server-api).

### TraceTail is not defined

Your code ran before the script. Put the script tag above your code, without `async` or `defer`. If you load it asynchronously, wait for its `load` event:

```html
<script id="tracetail" src="https://tracetail.io/sdk.js" data-api-key="YOUR_API_KEY" async></script>
<script>
  document.getElementById('tracetail').addEventListener('load', () => {
    TraceTail.generateFingerprint().then(({ visitorId }) => {
      // Send visitorId to your backend.
    });
  });
</script>
```

### Ad blockers and privacy extensions

Some extensions block third-party scripts or requests. If the script is blocked, `window.TraceTail` is undefined, so check for it before calling. If only the API call is blocked, the SDK still returns the visitor ID, with `error.code` set to `NETWORK_ERROR`. A [first-party setup](https://tracetail.io/docs#first-party) serves both the SDK and the API from your own domain, which blockers that filter by domain leave alone.

### Requests don't show in the dashboard

Check that the tag has `data-api-key` (without it the SDK runs keyless), then read `result.error`: its code says what went wrong (see [Errors](https://tracetail.io/docs#rest-api-errors)). `TraceTail.configure({ debug: true })` logs the details.

### Still stuck?

Email <support@tracetail.io> with the page URL, your browser and the `requestId` or `error.code`. The [live demo](https://tracetail.io/live-demo) shows what a working integration returns in your browser.

## Migrating from FingerprintJS

FingerprintJS is the open-source library from Fingerprint. TraceTail computes its own visitor ID, so existing FingerprintJS IDs don't carry over. Run both side by side for a while, then switch:

1. Add TraceTail next to your current integration (see [Install](https://tracetail.io/docs#install)).
2. Send both IDs with each event and store them together, for example for 30 days, so a returning visitor's records can be linked.
3. Move your lookups to the TraceTail `visitorId`, then remove FingerprintJS.

**Before: FingerprintJS**

```typescript
import FingerprintJS from '@fingerprintjs/fingerprintjs';

const fp = await FingerprintJS.load();
const { visitorId } = await fp.get();
```

**After: TraceTail**

```typescript
import { TraceTail } from '@tracetail/js';

const tracetail = new TraceTail({ apiKey: 'YOUR_API_KEY', endpoint: 'https://tracetail.io/api' });
const { visitorId } = await tracetail.generateFingerprint();
```

| FingerprintJS                        | TraceTail                       |
| ------------------------------------ | ------------------------------- |
| `result.visitorId`                   | `result.visitorId`              |
| `result.confidence.score` (0 to 1)   | `result.confidence` (0 to 1)    |
| `result.requestId` (Fingerprint Pro) | `result.requestId` (with a key) |

For prices and features side by side, see [TraceTail vs FingerprintJS](https://tracetail.io/vs-fingerprintjs).
