What Does the CreepJS API Actually Do?
The CreepJS API is an HTTP diagnostic service that recalculates a browser fingerprint ID from a snapshot you collect in a real browser, then returns the components, coverage and timings. It is a diagnostic, not a fraud score and not a login signal. You send a token in the X-API-Token header, post a payload under 128 KiB, and get back a report you can log, compare or display.

📌 TL;DR Executive Summary
- Core Takeaway: Token-authenticated diagnostic endpoint. Collect a real snapshot in a browser, post it to /v1/fingerprint with X-API-Token, and treat the returned ID as diagnostic evidence rather than proof of identity or fraud.
- Key Risk/Challenge: Authenticated calls share 1,000 requests per day per email across every token; submissions cap at 128 KiB; a network timeout is an uncertain outcome, so blind retries burn quota without telling you anything.
- Recommended Solution: Keep the token server-side, measure the serialized body before sending, retry only safe GETs, and log collector counts plus confidence before you compare two fingerprint IDs.
Before You Write Code: Token, Runtime, and Consent
The API only recalculates a snapshot that was collected in a real browser. That means two moving parts: a browser that produces the snapshot, and a server that holds your token and does the posting.
What you need in place
- Node.js 18 or newer — built-in fetch, AbortController and AbortSignal.timeout, so no HTTP library to install.
- A token issued to an email address you control, stored in an environment variable or secret manager.
- A snapshot JSON exported from the full checker or produced through the SDK.
- A payload you have already measured against the 128 KiB submission cap.
- Consent and a plain-language explanation for anyone whose browser you scan.
If you are still choosing a fingerprinting service rather than wiring one up, this browser fingerprinting API overview covers what these services measure and where they stop being useful. The endpoint reference for everything below is the API documentation.
Keep the token on the server
One naming note before you build on this. The .org domain hosts a separate educational diagnostic and API, distinct from Abraham Juliot’s GitHub research project. The code is MIT-licensed but the name is trademarked, and the GitHub project describes non-GitHub-Pages domains as unauthorized. That repository dates from 2020 and documents the browser tool, not this hosted API, so treat the hosted SDK as a third-party mirror and read it before you send anyone’s data through it.
Step 1: Get a CreepJS API Token
POST your email to /v1/token as JSON. The endpoint returns the token in the response body. Use it only when the email has no token yet, because issuance is limited and extra tokens do not raise the daily allowance.
const res = await fetch('https://api.creepjs.org/v1/token', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: process.env.CREEPJS_EMAIL })
});
if (!res.ok) throw new Error(`Token request failed: HTTP ${res.status}`);
const body = await res.json();
const token = typeof body === 'string' ? body : (body.token ?? body.apiToken);
if (typeof token !== 'string') throw new Error('No token in response body');
console.log(token); // first issuance only — move it to a secret manager now
Keep the print for that single run. After the first issuance, read the token from the environment at process start and never log it again — anything that reaches your console usually reaches your log aggregator too.
Step 2: Collect a Real Snapshot Before You Submit
Synthetic JSON will not do: the fingerprint endpoint expects a real collected snapshot. You have three routes, and they differ in where collection happens and what leaves the browser.
| Route | Where collection runs | What reaches the API |
|---|---|---|
| sdk.js (hosted loader) | Slim loader that fetches the full collectors on the first scan | The snapshot, when you request a fingerprint with a token |
| sdk.full.js (full bundle) | Entirely inside the browser tab | Nothing |
| Direct HTTP | In your own page or internal tool | The snapshot you post to /v1/fingerprint |
If you are weighing hosted against local collection, this compare fingerprinting APIs breakdown covers the trade-offs. The npm package documented as @creepjs/sdk is not published yet, so today you either load the hosted script or make direct HTTP calls. If you use collectFingerprint from @creepjs/core, the concurrency and idleDelay overrides change how collectors run; the forwarded payload stays the same shape.
Whichever route you pick, remember that the WebRTC collector in the full bundle still contacts Google STUN servers. That is a real request to a third party, so do not run it inside an air-gapped lab or a privacy-sensitive session without accounting for it.
import fs from 'node:fs/promises';
const raw = JSON.parse(await fs.readFile('snapshot.json', 'utf8'));
const data = raw.data && typeof raw.data === 'object' ? raw.data : raw;
const body = JSON.stringify({ data });
const bytes = Buffer.byteLength(body);
if (bytes > 128 * 1024) {
throw new Error(`Body is ${bytes} bytes; the API limit is 131072`);
}
console.log({ bytes, collectors: Object.keys(data).length });
Measure the serialized body you are about to send, not a summary of it. Missing values are not the same as false or zero, so keep the raw object intact and test with nullish checks instead of truthiness. The SDK caches results for 86,400 seconds by default, caps that cache at seven days, and treats cacheTtl: 0 as “no cache”; timeoutMs defaults to 10,000.
Step 3: Submit the Snapshot to /v1/fingerprint
Post the snapshot with your token in X-API-Token. The response carries a recalculated fingerprintId, the data object, coverage and timings. Match your body shape to the documentation on the first call: the wrapper below nests the collected snapshot under data, mirroring how the SDK returns it.
async function submitFingerprint(snapshot) {
const res = await fetch('https://api.creepjs.org/v1/fingerprint', {
method: 'POST',
headers: {
'content-type': 'application/json',
'X-API-Token': process.env.CREEPJS_API_TOKEN
},
body: JSON.stringify({ data: snapshot }),
signal: AbortSignal.timeout(15000)
});
const body = await res.json();
if (!res.ok) {
const error = new Error(`HTTP ${res.status}`);
error.status = res.status;
error.body = body;
throw error;
}
return body;
}
The SDK’s getFingerprint returns fingerprintId, data, confidence, collectors, timings, timestamp and cached — the same information with a wrapper around it. If the call comes back 413, your body is over 128 KiB: trim or split it, and never quietly drop components and then compare the shortened report as if it were complete.
Step 4: Query IP Context with /my-ip and /v1/ip/public/:ip
GET /my-ip needs no address and no token. It returns your public IP, approximate location and optional risk context. GET /v1/ip/public/:ip covers a specific address and allows 30 lookups per visitor per day. Both accept address literals only — not URLs, hostnames, ports or ranges.
import net from 'node:net';
async function getJson(path) {
const res = await fetch(`https://api.creepjs.org${path}`, {
headers: { accept: 'application/json' },
signal: AbortSignal.timeout(10000)
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
const ip = '203.0.113.10';
if (!net.isIP(ip)) throw new Error('Use an IP literal, not a URL or hostname');
const context = await getJson(`/v1/ip/public/${encodeURIComponent(ip)}`);
console.log(context);
Validate with net.isIP or an equivalent parser before the request, and keep encodeURIComponent so IPv6 literals survive the path. The site caches IP lookup responses for up to 24 hours, and one hour for high-risk results, so cache on your side instead of re-querying on every page load.
Step 5: Batch IP Lookups Without Losing Partial Results
POST /v1/ip/batch takes up to 50 IP literals per call and returns per-row outcomes. A failed row does not invalidate the rest of the batch, so keep the successes and record each failure alongside the input that produced it.
import net from 'node:net';
async function batchIps(ips, token) {
const valid = ips.filter((ip) => net.isIP(ip));
const rows = [];
for (let i = 0; i < valid.length; i += 50) {
const res = await fetch('https://api.creepjs.org/v1/ip/batch', {
method: 'POST',
headers: {
'content-type': 'application/json',
'X-API-Token': token
},
body: JSON.stringify({ ips: valid.slice(i, i + 50) })
});
const body = await res.json();
if (!res.ok) throw new Error(`Batch failed: HTTP ${res.status}`);
rows.push(...(body.results ?? body.rows ?? []));
}
return rows;
}
Filter invalid entries first: pass a hostname, URL, port or range and that row fails validation rather than being resolved for you. Chunk in fifties and preserve row order so you can map every result back to its input.
Step 6: Handle Quotas, Timeouts, and Error Codes
Authenticated requests share 1,000 requests per day across every token issued to the same email. A second token adds nothing. Public IP lookups have their own 30-per-visitor ceiling, and batch calls stop at 50 entries. Fingerprint payloads are stateless; what persists is your token record and a hashed daily counter that resets after 24 hours.
| Result | Meaning | Action |
|---|---|---|
| 400 | Invalid input | Validate IP literals, JSON and required fields before sending |
| 401 | Token rejected | Check the X-API-Token header and that the token matches the email |
| 413 | Payload over 128 KiB | Measure bytes, then trim or split — do not drop components silently |
| 429 | Quota reached | Stop and wait for the 24-hour reset; a new token will not help |
| Timeout | Uncertain for POST | Log the request, verify the outcome, do not auto-replay |
async function callWithGuards(fn) {
try {
return await fn();
} catch (err) {
if (err.status === 429) {
console.error('Quota reached — wait for the 24-hour reset; a second token adds nothing.');
} else if (err.status === 413) {
console.error('Body over 128 KiB — trim or split before retrying.');
} else if (err.code === 'request_timeout') {
console.error('Timeout is uncertain — do not replay a POST automatically.');
} else {
console.error(`Unhandled failure: ${err.message}`);
}
throw err;
}
}
Retry safe GETs with backoff. Do not replay a timed-out POST: a network timeout is an uncertain outcome and the request may already have been processed. Log the payload hash, the timestamp and the error, verify first, then decide. A 429 means waiting for the reset, never minting another token.
Step 7: Interpret fingerprintId, Data, and Confidence
fingerprintId is a diagnostic ID derived from the submitted components, so it can change when the environment or the available signals change. A new ID is not a new person and not a fraud event. confidence is the ratio of successful collectors between 0 and 1 — a measure of collection completion, not uniqueness. The documentation denies both the identity-proof and the fraud-probability reading outright.
function compareRuns(a, b) {
const aData = a.data ?? {};
const bData = b.data ?? {};
const keys = new Set([...Object.keys(aData), ...Object.keys(bData)]);
const missing = {};
for (const key of keys) {
const inA = Object.hasOwn(aData, key);
const inB = Object.hasOwn(bData, key);
if (!inA || !inB) missing[key] = { inA, inB };
}
return {
idChanged: a.fingerprintId !== b.fingerprintId,
confidenceA: a.confidence,
confidenceB: b.confidence,
missing
};
}
Before you compare two IDs, record the collector success, fail and skip counts plus timings. A missing collector and a false value produce different IDs for different reasons, which is why Object.hasOwn beats a truthiness test. If you need a vendor-level view of how returning-visitor signals are usually weighed, this fingerprinting APIs for returning visitors guide is a useful companion.
When your checks span browser profiles you own or manage, Send.win’s desktop app keeps each profile isolated with its own cookies, storage, session, fingerprint settings and proxy. You run the collector by hand in each profile and submit the exported snapshot from your own server. Because every scan is started by a person inside the browser, the loop stays slow enough to be auditable and each result stays tied to a session you can point at.
Common Errors and How to Fix Them
Most failures are credentials, payload size, or a retry that treated an uncertain outcome as a safe one. Fix the cause before you touch the code.
- 401 token rejected. The token is missing, wrong, or sent as a query parameter. Use the X-API-Token header with the token issued for that email.
- 413 payload too large. Measure the serialized body with Buffer.byteLength. Remove oversized fields or split the report; do not drop collectors and then compare the result as if it were complete.
- 429 quota reached. The email has spent its 1,000 authenticated requests for the day. Wait for the reset and cut calls; another token does not add quota.
- request_timeout. The SDK passed timeoutMs, or your fetch aborted. On POST that is uncertain — log it and verify before sending again.
- api_error. A non-2xx response. Read the status and body; 400 and 401 will not fix themselves on a retry.
- core_load_failed. The slim bundle could not fetch the full collectors. Check fullBundleUrl, your CSP and the network path, or use the local bundle instead.
- invalid_response. The payload was not the shape the SDK expected. Check content-type, JSON validity and how you wrapped the snapshot.
- IP lookup 400. You sent a hostname, URL, port or range. Pass an IPv4 or IPv6 literal; net.isIP catches it before the request leaves your process.
If the shared daily allowance keeps blocking legitimate work, weigh a different hosted option before you build a queue around it — this CreepJS alternative breakdown compares what else is available and what each service actually returns.
The Full Working Script
The Node 18+ script below issues a token, loads and validates a snapshot, submits it, queries public IP context, batches IP lookups, and summarizes collector counts. Save it as creepjs-api.mjs and run one command at a time.
How Send.win Helps With Creepjs Api
Send.win is a browser profile manager for professionals and teams: every account gets its own isolated browser profile.
- Isolated profiles – separate cookies, storage and sessions per profile
- Fingerprint protection – each profile keeps its own consistent browser fingerprint, so profiles never share one browser identity
- Desktop app + cloud browser – native app for Windows, macOS and Linux, or run profiles in the cloud with no install
- Residential proxies included – on Pro and Team, with timezone and locale matched to the proxy
- Team sharing – share a signed-in profile with a teammate without sharing the password
Try the free cloud demo — no install — or download the desktop app. Paid plans start at $19/month, and your first card purchase is covered by a 7-day money-back guarantee (see pricing).
import fs from 'node:fs/promises';
import net from 'node:net';
import process from 'node:process';
const BASE = 'https://api.creepjs.org';
const TOKEN = process.env.CREEPJS_API_TOKEN;
const EMAIL = process.env.CREEPJS_EMAIL;
async function request(path, { method = 'GET', body, auth = true, timeoutMs = 10000 } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const headers = { accept: 'application/json' };
if (body !== undefined) headers['content-type'] = 'application/json';
if (auth) {
if (!TOKEN) throw new Error('Set CREEPJS_API_TOKEN');
headers['X-API-Token'] = TOKEN;
}
const res = await fetch(`${BASE}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
signal: controller.signal
});
const text = await res.text();
let data;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = text;
}
if (!res.ok) {
const error = new Error(`HTTP ${res.status}`);
error.status = res.status;
error.body = data;
throw error;
}
return data;
} catch (err) {
if (err.name === 'AbortError') {
const error = new Error(`Request timed out after ${timeoutMs} ms`);
error.code = 'request_timeout';
error.uncertain = method === 'POST';
throw error;
}
throw err;
} finally {
clearTimeout(timer);
}
}
async function issueToken(email) {
return request('/v1/token', { method: 'POST', auth: false, body: { email } });
}
async function loadSnapshot(file) {
const raw = JSON.parse(await fs.readFile(file, 'utf8'));
const data = raw.data && typeof raw.data === 'object' ? raw.data : raw;
const payload = { data };
const bytes = Buffer.byteLength(JSON.stringify(payload));
if (bytes > 128 * 1024) {
throw new Error(`Snapshot body is ${bytes} bytes; limit is 131072`);
}
return payload;
}
async function submitFingerprint(payload) {
return request('/v1/fingerprint', { method: 'POST', body: payload, timeoutMs: 15000 });
}
async function myIp() {
return request('/my-ip', { auth: false });
}
async function publicIp(ip) {
if (!net.isIP(ip)) throw new Error(`Not an IP literal: ${ip}`);
return request(`/v1/ip/public/${encodeURIComponent(ip)}`, { auth: false });
}
async function batchIps(ips) {
const valid = ips.filter((ip) => net.isIP(ip));
const rows = [];
for (let i = 0; i < valid.length; i += 50) {
const result = await request('/v1/ip/batch', {
method: 'POST',
body: { ips: valid.slice(i, i + 50) }
});
rows.push(...(result.results ?? result.rows ?? []));
}
return rows;
}
function summarizeCollectors(collectors = {}) {
const counts = { success: 0, failed: 0, skipped: 0, unknown: 0, total: 0 };
for (const value of Object.values(collectors)) {
counts.total += 1;
const status = typeof value === 'string' ? value : value?.status;
if (status === 'success' || status === 'ok') counts.success += 1;
else if (status === 'failed' || status === 'error') counts.failed += 1;
else if (status === 'skipped') counts.skipped += 1;
else counts.unknown += 1;
}
return counts;
}
async function main() {
const [command, arg] = process.argv.slice(2);
if (command === 'token') {
if (!EMAIL) throw new Error('Set CREEPJS_EMAIL');
console.log(JSON.stringify(await issueToken(EMAIL), null, 2));
return;
}
if (command === 'fingerprint') {
if (!arg) throw new Error('Usage: node creepjs-api.mjs fingerprint snapshot.json');
const payload = await loadSnapshot(arg);
const result = await submitFingerprint(payload);
console.log(JSON.stringify({
fingerprintId: result.fingerprintId,
confidence: result.confidence,
collectors: summarizeCollectors(result.collectors),
timings: result.timings,
timestamp: result.timestamp
}, null, 2));
return;
}
if (command === 'ip') {
const result = arg ? await publicIp(arg) : await myIp();
console.log(JSON.stringify(result, null, 2));
return;
}
if (command === 'batch') {
const ips = arg ? arg.split(',') : [];
console.log(JSON.stringify(await batchIps(ips), null, 2));
return;
}
throw new Error('Commands: token | fingerprint <file> | ip [address] | batch ip1,ip2');
}
main().catch((err) => {
console.error(JSON.stringify({
message: err.message,
status: err.status,
body: err.body,
code: err.code,
uncertain: err.uncertain ?? false
}, null, 2));
process.exitCode = 1;
});
Run it like this:
export CREEPJS_API_TOKEN="your-token"
export CREEPJS_EMAIL="[email protected]"
node creepjs-api.mjs token
node creepjs-api.mjs fingerprint snapshot.json
node creepjs-api.mjs ip
node creepjs-api.mjs ip 203.0.113.10
node creepjs-api.mjs batch 203.0.113.10,198.51.100.7
The token command is for the first issuance only; after that, load CREEPJS_API_TOKEN from your secret manager. The script never retries a fingerprint POST. It prints the error, the status and whether the outcome is uncertain, then exits so you can verify before spending another request.
🏆 Send.win Verdict
A CreepJS report only means something when the snapshot came from a real, consented browser session, and producing that session is manual work. Send.win is a browser profile manager, not a CreepJS integration — every request in this guide is made from your own Node.js server against the public HTTP endpoints, and every snapshot is collected by hand inside a browser profile. What Send.win gives you is isolation: in the desktop app each profile has its own cookies, storage, session, fingerprint settings and proxy, and the cloud browser runs on an EU server in Helsinki for machines where you cannot install anything. You open a profile yourself, run the collector in it, and post the snapshot from your own server, so every result stays tied to a session you can point at and audit.
See Send.win plans — Pro from $19/mo with a 7-day money-back guarantee on your first card purchase, or start with the free cloud demo at 10 minutes a day.
Frequently Asked Questions
How do I get a CreepJS API token?
POST your email to https://api.creepjs.org/v1/token as JSON. The token comes back in the response body. Keep it server-side and send it in the X-API-Token header; issuance is limited, and multiple tokens for one email share the same daily allowance.
What does the CreepJS fingerprint API return?
It returns a recalculated fingerprintId, the data object with the collected components, coverage or confidence, timings and a timestamp. The SDK wrapper also adds collectors and a cached flag. The ID is diagnostic, not a fraud score and not proof of identity.
Why do I get a 429 from the CreepJS API?
The email behind your token has used its 1,000 authenticated requests for the day, or you hit the 30-per-visitor public IP lookup limit. Extra tokens do not raise the authenticated allowance. Wait for the 24-hour reset and cache or batch your calls.
How do I fix a 413 payload too large error?
Measure the serialized body with Buffer.byteLength before sending; the submission limit is 128 KiB. Trim oversized fields or split the report, but do not silently drop collectors and treat the shortened result as equivalent to a full scan.
What is the difference between sdk.js and sdk.full.js?
sdk.js is a slim loader that fetches the full collectors on the first scan. sdk.full.js computes the report locally and sends nothing to the API. The full bundle still makes a Google STUN request for WebRTC, so check your network and privacy requirements first.
Can I use the CreepJS API from Python or Node?
Yes. It is an HTTP API, so Node’s built-in fetch or Python’s requests library can call it. Send the token in the X-API-Token header rather than the query string. The npm SDK package is not published yet, so direct HTTP calls are the practical route today.
Is a CreepJS fingerprint ID a fraud score?
No. fingerprintId is a diagnostic ID derived from the submitted components, and confidence is the ratio of successful collectors. The documentation explicitly denies identity-proof and fraud-probability readings. A changed ID can simply mean the environment or the available signals changed.
How do I run CreepJS without sending data anywhere?
Use sdk.full.js, the full local bundle. It computes the report in the browser and sends nothing to the API. The WebRTC collector still contacts Google STUN servers, and the full checker stores the last 50 scans in localStorage after stripping WebRTC addresses and media device labels.