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
<scriptsrc="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
<scriptsrc="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>
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.
Bundling your JavaScript? 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. For local development, add a key for localhost. Your backend uses a different, secret key with the 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.
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.
Place the tag before </body>. It loads synchronously, so TraceTail is ready on the next line.
index.html
<scriptsrc="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
curl-s https://tracetail.io/sdk/v3.1.2/sdk.js | openssl dgst -sha384-binary | openssl base64 -A
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.
With server-side rendering, ngOnInit also runs on the server, where there is no browser to identify. Call generateFingerprint() from afterNextRender() instead.
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 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.
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
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:
Terminal
claude mcp add --transport http tracetail https://tracetail.io/mcp
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:
{"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…"}
{"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. 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
npx skills add https://tracetail.io
Docs for LLMs
/llms.txt: what TraceTail is, a quick start and links to everything. /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 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 and choose First-party next to the site's key:
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.
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:
worker.js
// 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;exportdefault{asyncfetch(request, env){const base ='/'+String(env.TRACETAIL_PATH ||'').replace(/^\/+|\/+$/g,'');const url =newURL(request.url);if(base ==='/'||!url.pathname.startsWith(base +'/'))returnnotFound();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')returnnewResponse(null,{status:405,headers:{Allow:'GET, HEAD'}});// Only what lets the browser revalidate the copy it holds (TraceTail answers 304).returnpassThrough(awaitfetch(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 =awaitforwardedRequest(request, env);if(!forwarded)return Response.json({"error":"Request body is too large.","code":"PAYLOAD_TOO_LARGE"},{status:413});returnpassThrough(awaitfetch(upstream + target, forwarded));}returnnotFound();},};// Only the headers TraceTail needs: never your site's cookies or other credentials.// Null when the body is larger than TraceTail accepts.asyncfunctionforwardedRequest(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 ?awaitreadBody(request):undefined;return body ===null?null:{method: request.method, headers, body };}functioncopied(request, names){const headers =newHeaders();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.asyncfunctionreadBody(request){if(Number(request.headers.get('content-length')||0)> MAX_BODY_BYTES)returnnull;if(!request.body)returnundefined;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();returnnull;}
chunks.push(value);}const body =newUint8Array(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.functionpassThrough(response){const headers =newHeaders();for(const name of FORWARDED_RESPONSE_HEADERS){const value = response.headers.get(name);if(value) headers.set(name, value);}returnnewResponse(response.body,{status: response.status,statusText: response.statusText, headers });}functionnotFound(){returnnewResponse('Not found',{status:404});}
app/k3v9/[...route]/route.ts
// 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;asyncfunctionproxy(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;elseif(API_PATHS.includes(path)) target = path;elseif(path ==='/health') target ='/api/proxy/status';elsereturnnewResponse('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 =newHeaders();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)returnpassThrough(awaitfetch(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 ?awaitreadBody(request):undefined;if(body ===null)return Response.json({"error":"Request body is too large.","code":"PAYLOAD_TOO_LARGE"},{status:413});returnpassThrough(awaitfetch(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.functionpassThrough(upstream: Response): Response {const responseHeaders =newHeaders();for(const name of FORWARDED_RESPONSE_HEADERS){const value = upstream.headers.get(name);if(value) responseHeaders.set(name, value);}returnnewResponse(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.asyncfunctionreadBody(request: Request): Promise<ArrayBuffer |null|undefined>{if(Number(request.headers.get('content-length')||0)> MAX_BODY_BYTES)returnnull;if(!request.body)returnundefined;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();returnnull;}
chunks.push(value);}const body =newUint8Array(size);let offset =0;for(const chunk of chunks){
body.set(chunk, offset);
offset += chunk.byteLength;}return body.buffer;}exportconst GET = proxy;exportconst HEAD = proxy;exportconst 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
<scriptsrc="/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>
Open the health check on your site. trusted means TraceTail accepts the proxy's secret, and visitorIp should be your own IP address:
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.
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.
isBot
boolean?
Keyed mode: true for automated browsers (headless Chrome, WebDriver).
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.
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, 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.
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:
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
{"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 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: consistentand 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.
The Server API takes a server key: tts_ followed by 64 hexadecimal characters. Create one in Settings → API keys, 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
{"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, 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). 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.
verify.js
const TEN_MINUTES_MS =10*60*1000;// requestId and visitorId as your page sent them.asyncfunctionverifyIdentification(requestId, visitorId){const response =awaitfetch(`https://tracetail.io/api/v1/identifications/${encodeURIComponent(requestId)}`,{headers:{Authorization:`Bearer ${process.env.TRACETAIL_SERVER_KEY}`}},);if(response.status ===404)returnfalse;// not an identification in your accountif(!response.ok)thrownewError(`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'));}
verify.py
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.defverify_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:returnFalse# 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
GET https://tracetail.io/api/v1/visitors/fp3_e8fc80d60f8c3571
Authorization: Bearer tts_…
200 OK
{"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) 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, 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.
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:
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.
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, '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.
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:
<scriptid="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 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). 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 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:
Add TraceTail next to your current integration (see Install).
Send both IDs with each event and store them together, for example for 30 days, so a returning visitor's records can be linked.
Move your lookups to the TraceTail visitorId, then remove FingerprintJS.