Docs
Integrate in two calls.
One script tag collects signals in the browser and returns a requestId. Your backend then fetches the authoritative result with a secret key. Never trust the client alone.
Load the agent in the page
Paste this before the closing </body> tag. Replace pk_YOUR_PUBLIC_KEY with a public key from your dashboard.
<script src="https://api.whorlid.com/v1/fp.js"></script>
<script>
fpjs
.load({ publicKey: 'pk_YOUR_PUBLIC_KEY' })
.then(function (fp) { return fp.get(); })
.then(function (result) {
// result.requestId - send this to your backend for verification
// result.visitorId - linked id for this browser
// result.similarity - 0..1 signal-agreement score
console.log(result.requestId, result.visitorId, result.similarity);
});
</script>Public keys are safe to expose in the browser. Restrict which sites can use them with the allowed-origins list in your project settings.
Verify with your secret key
The client response is a hint, not a proof. Send the requestId to your backend and fetch the authoritative event with your secret key:
curl https://api.whorlid.com/v1/events/{requestId} \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"Response:
{
"requestId": "0d9e2c9b-...",
"visitorId": "7a41f3d2-...",
"similarity": 0.98,
"decision": "match",
"isReturning": true,
"highConfidence": true,
"bucketRarity": 0.92,
"isBot": false,
"botScore": 0,
"botReasons": [],
"threatLevel": "low",
"country": "IE",
"isDatacenter": false,
"isTor": false,
"ipCategory": "unknown",
"asn": null,
"asnOrg": null,
"isProxy": false,
"proxyScore": 0,
"proxyReasons": [],
"ip": "203.0.113.7",
"userAgent": "Mozilla/5.0 ...",
"matcherVersion": "v2",
"origin": "https://app.example.com",
"createdAt": "2026-07-20T09:30:00.000Z"
}Secret keys (sk_live_...) must never ship to the browser. Create one in your project's API keys tab; it is shown exactly once.
Response field reference
| Field | Type | Description |
|---|---|---|
| requestId | string (uuid) | Id of this identification event. Use it for server-side verification. |
| visitorId | string (uuid) | Identifier the matching engine linked this browser to. It is a probabilistic link, not a permanent hardware id: browsers with anti-fingerprinting protections can receive a new id, and two identically configured devices can be indistinguishable. |
| similarity | number (0 to 1) | Weighted agreement between this visit's signals and the matched profile. It is a similarity score, not a calibrated probability; compare it against your own thresholds per use case. |
| decision | "match" | "new" | "ambiguous" | How the engine resolved this observation (server-verified response only). match: linked to a known device. new: a confidently distinct device. ambiguous: evidence or the margin over the second-best candidate was insufficient, so a fresh id was issued as a fallback. Treat ambiguous as low-trust, not a confident new device. |
| isReturning | boolean | True when the visitor was matched to a previously seen device. |
| highConfidence | boolean | True when similarity clears the engine's strict match threshold. |
| ip | string | IP address seen by the API (server-verified response only). |
| userAgent | string | Browser user agent string (server-verified response only). |
| origin | string | Origin of the page that ran the identification. |
| createdAt | string (ISO 8601) | When the identification event was recorded. |
Install with AI
Working with a coding agent? Paste this prompt into Claude Code, Cursor, or any AI assistant with access to your codebase. It points the agent at llms.txt (this documentation in machine-readable form) and walks it through both the browser snippet and the server-side verification.
Integrate WhorlID (a device identification API for fraud prevention) into this codebase, end to end.
Full API reference: https://whorlid.com/llms.txt
Fetch and read that file first. Then:
1. FRONTEND. On the pages where I need device recognition (signup, login, checkout), load the agent and collect a requestId:
<script src="https://api.whorlid.com/v1/fp.js"></script>
<script>
fpjs.load({ publicKey: 'PK_PLACEHOLDER' })
.then(fp => fp.get())
.then(result => {
// send result.requestId to the backend with the form/request it belongs to
});
</script>
The public key is safe to expose. Send requestId alongside my existing form submit or API call; do not act on any other field in the browser.
2. BACKEND. Wherever the requestId arrives, verify it server side before trusting it:
GET https://api.whorlid.com/v1/events/{requestId}
Authorization: Bearer SK_PLACEHOLDER
Store the secret key in an environment variable, never in client code or the repo. The response is the authoritative event; the browser copy is a hint that must not be trusted.
3. USE THE RESULT. Key response fields:
- visitorId: stable id for this device in this project
- decision: match | new | low | ambiguous
- similarity (0..1) and highConfidence: how strong the match evidence is
- isReturning: whether this device has been seen before
- isBot, botScore (0..1): automation markers
- threatLevel: low | medium | high (bot only reaches high)
- country, isDatacenter: network context
The verify endpoint above also returns isTor, ipCategory, asn, asnOrg,
isProxy, proxyScore, and proxyReasons. These are on this server-verified
response only, never in the browser result, so never trust them from the
client.
Suggested starting policy:
- decision === 'match' && !isBot: treat as a known returning device
- isBot: challenge or route to review
- isTor || isProxy || isDatacenter: apply your own policy (added friction,
step-up auth, a review queue), not an automatic block; these describe
the network, not a confirmed fraud attempt
- everything else: proceed normally, log visitorId for pattern analysis
4. WIRE MY USE CASE. Ask me which problem I am solving (trial abuse, account takeover, multi-accounting, bot filtering) and add the matching lookup, e.g. counting existing accounts per visitorId at signup.
Replace PK_PLACEHOLDER and SK_PLACEHOLDER with the keys from https://whorlid.com/dashboard (Project > API keys), and tell me what framework this project uses if it is not obvious from the code.The collector is open source
The agent that runs in your visitors’ browsers is published under MIT as @whorlid/whorlid. You can read every probe before you ship it, and there is no build step or minified blob to take on trust.
npm install @whorlid/whorlidIt also runs with no account at all, computing an id in the browser and making no network request. That is genuinely useful for local development and for deciding whether fingerprinting suits your problem before you sign up for anything. What it cannot do is recognise a device across visits: a local hash has nothing to compare against, so a browser update or a driver change reads as a new device. Passing a public key is what switches on server-side matching, and that is the whole difference between the library and this service.
See it identify your own browser with no account, or read the source on GitHub.
Smart signals
Every /v1/identify call also returns risk and rarity signals computed at identification time:
| Field | Type | Description |
|---|---|---|
| isBot | boolean | True when the browser looks automated (webdriver, headless, and similar). |
| botScore | number (0 to 1) | Continuous bot likelihood behind isBot (server-verified response only). A strong signal (automation user agent, webdriver) scores 1; weaker hints accumulate. Apply your own threshold rather than relying only on the boolean. |
| threatLevel | "low" | "medium" | "high" | Coarse risk rating composed from the other signals. |
| bucketRarity | number (0 to 1) | How rare this fingerprint's coarse hardware bucket is among your visitors. A device alone in its bucket scores near 1; a very common profile trends toward 0. It is an occupancy measure, not an entropy estimate. |
| country | string | null | ISO 3166-1 alpha-2 country code derived from the IP, when known. Includes GeoLite2 data created by MaxMind, available from maxmind.com. Resolved on our own servers, so no IP is sent to a third party. |
| isTor | boolean | True when the IP is a currently published Tor exit node (server-verified response only). This is checked against the Tor Project's own bulk exit list, refreshed hourly: an exact match, not a heuristic. If a refresh ever fails, the last good list is kept and this answers false rather than guess, so it can under-flag but never invents a positive. |
| ipCategory | "residential" | "hosting" | "tor" | "unknown" | Network category for the IP (server-verified response only). In practice residential is never returned: we can prove an IP belongs to hosting or cloud infrastructure, we cannot prove one belongs to a home connection, so an IP outside our dataset comes back unknown rather than a guess at residential. |
| asn | number | null | Autonomous system number of the network operator, from a locally held dataset (server-verified response only). null when the IP is not in the dataset. |
| asnOrg | string | null | Name of the network operator for that ASN, for example "Amazon AWS" or "Cloudflare" (server-verified response only). null when asn is null. |
| isProxy | boolean | True when network and browser signals disagree enough to suggest the traffic is proxied (server-verified response only): a Tor exit on its own, or a browser timezone country that disagrees with the IP's country while the IP is hosting infrastructure. Weaker signals, such as a bare country mismatch or a hosting IP alone, only flag when two of them agree. See proxyReasons for which rules fired. |
| proxyScore | number (0 to 1) | Severity-weighted score behind isProxy (server-verified response only). A decisive signal scores 1 on its own; weaker hints need to accumulate. Apply your own threshold rather than relying only on the boolean. |
| proxyReasons | string[] | Which rules fired, for example "tor exit node" or "timezone country disagrees with IP country" (server-verified response only). |
The browser response keeps the detection details private. Your backend can fetch the full record, including botReasons, isDatacenter, isTor, ipCategory, asn, asnOrg, isProxy, proxyScore, and proxyReasons, via GET /v1/events/{requestId} with a secret key. threatLevel is the only one of these that also reaches the browser, at medium rather than high for Tor or proxy evidence on its own: using a privacy network is not by itself evidence of fraud, so apply your own policy to the raw fields.
Two limits worth knowing before you rely on this. Residential proxies are not detectable this way: a proxy running on a real home connection looks exactly like one, so we flag Tor exits, hosting and cloud ranges, and most commercial VPNs through the location check, and stop there. That location check is also deliberately blind between countries that share an IANA timezone, for example Europe/Berlin (DE, DK, NO, SE, SJ) or Asia/Tokyo (JP, AU), which is why a bare country mismatch is a weak signal rather than a decisive one: it also catches neighbouring-country travel and EU roaming, not just proxies.
What verification does and does not prove
Fetching an event with your secret key proves one thing: this service recorded that identification call, with these signals, from that IP, at that time. It does not prove the signals came from the physical device they describe. Everything the browser agent collects is public JavaScript output, so a motivated attacker can replay or fabricate signals with a valid public key.
Treat the result as one strong risk input alongside your own session, account, and velocity data. Use it to add friction (step-up auth, review queues, rate limits), not as the sole reason to hard-block a user. For account-security decisions, pair the visitorId with a first-party identifier you control.