Zeno dokumentacija
Dohvati API ključeve

Poslužitelj

Smart signals

What each flag means and how sure it is, how the risk score adds up, and what to do with it.

Every event carries detected signals, a riskScore and a bot verdict, worked out on the server. They only come back through the server API and webhooks (and, encrypted, in sealed results), never readable in the browser, so a bot can't tune itself against them.

The page goes from the parts to the decision: the signals, how they add up to the risk score, the bot verdict, velocity, what to do without a request ID, and recipes per action. The sections after the recipes go deeper into single signals.

Start from your action

The risk score is a generic starting point: one number for every kind of site, which can't know what you're protecting. A VPN means little on a login and more on a payment; a second account on one browser is fine for a family streaming service and the whole problem for a free trial. So decide per action:

  1. pick the signals, velocity counts, related IDs and returning that matter for this action (the recipes below are a start);
  2. use the score as a quick default where you haven't decided yet, or as a tiebreaker;
  3. check against your own traffic on the dashboard, and adjust.

Detected

FamilySignalDashboardMeansWeight
automationautomationAutomationA script drives the browser: leftovers of WebDriver, Selenium, Playwright, Puppeteer, PhantomJS or Nightmare.–
headlessHeadlessA browser without a window, as scripts and scrapers run it; its user agent says so.–
devicevirtual-machineVirtual machineThe browser runs in a virtual machine: a VM graphics adapter, or a virtual machine score of 0.6 or more.–
software-rendererSoftware rendererGraphics are drawn by the processor, not a graphics card: headless browsers, VMs, device farms.–
networktorTorThe IP is a Tor exit node.–
proxyProxyThe IP is on the open proxy lists.–
datacenterDatacenterThe IP belongs to a hosting network rather than a home or mobile one, and isn't a known VPN exit: mostly servers, also cloud desktops and some company networks.–
abusive-ipAbuser IPThe IP itself is on abuse blocklists (spam, attacks, scanning), according to the IP lookup.–
residential-proxyResidential proxyA home or office internet provider's IP whose time zone doesn't fit the browser's: a residential proxy, or a traveller.–
vpnVPNThe IP is a known VPN exit. It replaces datacenter, since nearly every exit runs on a hosting network.–
private-relayPrivate RelayiCloud Private Relay: Apple's privacy relay, on for a large share of Safari users. It replaces vpn and datacenter.–
timezone-mismatchTime zone mismatchThe browser's clock is set to a time zone with another UTC offset than the connection's.–
environmenttamperingTamperingThe browser lies about itself: built-in functions replaced by script, values that differ between the page and its workers or frames, or signals that contradict each other. Anti-detect browsers and fraud tools.–
developer-toolsDeveloper toolsThe browser's developer tools are open.–
incognitoIncognitoA private or incognito window, where the browser gives it away.–
privacy-settingsPrivacy settingsCanvas, WebGL or audio reads come back randomized or replaced: a privacy browser or extension.–
privacy-browserPrivacy browserBrave, DuckDuckGo, Tor Browser, Mullvad Browser or Firefox with fingerprinting resistance. Context, no points.–
rare-deviceRare deviceHardware hardly any other visitor of the project has. Context, no points.–
evidencereplayedReplayedThe same request was sent before, within 7 days: a captured identification sent again. Also a bad bot.–
ai-agentAI agentAn AI agent says it drives the browser, like ChatGPT agent or Operator.–
socket-proof-failedSocket proof failedWith socket proof on, the browser didn't complete the WebSocket challenge: a wrong or used challenge, a socket that failed or answered too late, or no socket at all.–
tls-mismatchTLS mismatchThe connection's TLS handshake doesn't fit the browser it claims to be: a script posting a browser's report, or a network that intercepts TLS.–

The weights are the defaults; a project can change them in Settings. The dashboard's Smart signals panel groups them its own way: it adds Bot (the bot verdict), High activity (24h.events of 100 or more) and Socket proof (socketProof), and leaves some signals out of its list. The IP signals (tor, proxy, datacenter, abusive-ip, vpn) come from ipapi.is, abusive-ip being its own verdict on the IP. A known VPN exit is vpn and never also datacenter, and iCloud Private Relay is private-relay in place of both. When the lookup is slow or down, identification goes on without them. Under a VPN, proxy, Tor or Private Relay, the event's location is the exit's, not the person's.

How sure, and what to do

For a reviewer looking at one case, where one signal stands out:

FlagHow sureWhat to do
automationNear certain when set. Stealth tools that hide every leftover get past it.Block or challenge.
headlessCertain for browsers that say so; stealth headless browsers hide it.Block or challenge.
virtual-machineFairly sure, but developers, testers and some company desktops use VMs.Challenge sensitive actions.
software-rendererSure of what it measures; old or remote desktops do it too.Ignore alone; weigh with VM or automation.
torSure: exit lists are public and current.Challenge; block for payouts and payments.
proxyFairly sure; lists lag behind new proxies.Challenge.
datacenterSure of the network; with VPN exits left out, what stays is mostly servers running scripts, with some cloud desktops and company networks.Challenge.
abusive-ipAs good as the blocklists; a shared or recycled IP may carry another user's history.Weigh with other signals.
residential-proxyA guess from the time zone; travellers set it off.Weigh with other signals.
vpnSure for known VPNs; lists lag behind new exits.Ignore alone; many real users have one.
private-relaySure.Ignore; never block.
timezone-mismatchSure of the mismatch; travellers and VPN users have one.Ignore alone.
tamperingFairly sure; a few privacy extensions set it off.Challenge; review with other signals.
developer-toolsFair; detection varies by browser.Ignore alone; often your own staff.
incognitoSure when set; a Chromium private window can pass for a normal one.Ignore alone.
privacy-settingsSure of what it measures.Ignore alone.
privacy-browserSure for browsers that say so.Ignore; context only.
rare-deviceNeeds a project with 10,000 visitors; often just a new model.Context only.
replayedCertain.Block.
ai-agentA claim, not checked against the agent.Decide per action: reading yes, signing up or paying maybe not.
socket-proof-failedGood against replayed and forged identifications; a socket a network blocks leaves it off.Weigh it.
tls-mismatchGood against scripts that post a captured report; company networks that inspect TLS set it off too.Weigh it.

software-renderer, timezone-mismatch, privacy-settings, incognito and vpn alone also fit regular users on VMs, VPNs or privacy browsers, and datacenter alone fits cloud desktops and some company networks. Weigh them together with the rest of the event.

Risk score

Signals that say the same thing in different ways are grouped in families, so one cause never piles up points: a headless browser is automation however many of its tells fire, and a Tor exit on abuse lists is one connection, not two. Within a family the strongest flag counts in full and every further flag with a weight adds one point, up to the family's cap. The families add up to the score, which runs from 0 to 100: the sum of the fired flags' points, capped at 100. riskScoreBreakdown lists what each fired flag added, strongest first.

FamilySignalsCap
automationautomation, headless15
devicevirtual-machine, software-renderer8
networktor, proxy, datacenter, abusive-ip, residential-proxy, vpn, private-relay, timezone-mismatch12
environmenttampering, developer-tools, incognito, privacy-settings, privacy-browser, rare-device8
evidencereplayed, ai-agent, socket-proof-failed, tls-mismatchnone, they add up

For example, a server on a hosting network that abuse lists know, browsing from another time zone than its IP's, has datacenter (9), abusive-ip (5) and timezone-mismatch (2): the network family takes the strongest, 9, plus one point for each of the other two, so the score is 11 and riskScoreBreakdown.points lists datacenter with 9 points and the other two with 1 each. A known VPN exit is vpn without datacenter, so a Mullvad exit on abuse lists scores 6 (abusive-ip, 5, and one point for vpn). A bad bot scores at least 15, whatever its flags weigh; riskScoreBreakdown.addedForBadBot says by how much it was topped up, and removedByCap what a project's own heavy weights would have added past 100.

Typical scores of real users

WhoSignalsScore
Most peoplenone0
iCloud Private Relayprivate-relay1
Braveprivacy-browser, maybe privacy-settings0 to 1
A private windowincognito2
An ordinary VPNvpn2
A VPN whose exit is in another time zonealso timezone-mismatch3
A VPN on an exit abuse lists knowvpn, abusive-ip6
Both of the last twoall three7
A cloud desktop or a company network on a hosting providerdatacenter9

Starting bands

As a default for a login or signup, until you've decided per signal:

  • below 9: let them through. Most real users score a few points at most, and an ordinary VPN lands here too, whatever its exit (2 to 7).
  • 9 to 14: challenge with a second factor, a captcha or a review: a Tor exit (12), an open proxy (10) or a hosting network that isn't a VPN (9), a tampered browser with another signal (9 and up).
  • 15 and up: block or review. A bad bot, or strong signals from two families at once.

The dashboard colours scores with its own, gentler bands: Low risk below 5, Some risk from 5 to 14, High risk from 15 or for a bad bot. They describe the score; the bands above are a decision.

A connection alone never reaches 15, since the network family is capped at 12. To treat VPN users differently, don't lower the thresholds, which would catch every one of them: read the detected signals (vpn, private-relay, tor) or the network family's points in riskScoreBreakdown instead.

Changing the weights

A project can change any signal's weight from 0 to 20 in Settings › Risk score; an empty field keeps the default, and a weight above a family's cap still counts in full. Raise vpn if VPN users are your fraud, or set incognito to 0 if private windows are normal for you.

New weights only apply to events identified after the change. The score and riskScoreBreakdown are worked out once, when the event is identified, and stored: older events keep the score they were given, in the API, in search and on the dashboard. To compare, filter by date.

Your own traffic sets the real thresholds: the dashboard's event page says how many of the project's events in the last 30 days scored lower than this one, and how many the same.

Bot verdict

botWhen
badA script drives the browser (automation or headless, AI agents that take a browser over this way included), or the request was sent before (replayed). A request that claims to be a crawler, like Googlebot, from an IP that isn't that crawler's is bad too.
goodA crawler the IP lookup knows, from its own network.
not_detectedNeither. That includes an AI agent that only says what it is (it gets the ai-agent signal), and a claimed crawler when the IP lookup had no answer.

Velocity

GET /events/{requestId} also returns velocity: how busy the event's visitor, IP and external ID were in the 5 minutes, hour and 24 hours up to and including this event, under the keys 5m, 1h and 24h. A first event counts itself, so every count is at least 1. It's worked out when you read the event, so search, webhooks and sealed results don't carry it. Many events, several external IDs or several countries in a short window point at account farming or credential stuffing.

CountOfCounts
eventsThe visitorIts events
distinctIpThe visitorThe IPs it came from
distinctExternalIdThe visitorThe external IDs it was seen with
distinctCountryThe visitorThe countries of its IPs
ipEventsThe IPEvents from this IP, every visitor's
distinctIpByExternalIdThe external IDThe IPs the event's external ID came from
distinctVisitorIdByExternalIdThe external IDThe visitors (browsers) it was seen with

Worth a look, as a start:

CountWorth a look fromPoints at
24h.distinctExternalId3One browser making or using many accounts
1h.distinctCountry2A VPN hopping exits, or a shared session
5m.events10A script, or a page calling get() in a loop
24h.events100What the dashboard calls high activity
24h.distinctVisitorIdByExternalId4One account used from many browsers: shared or taken over
1h.ipEventsFar above your normalA busy IP; an office or mobile network can be normal

These are starting points. Your own test logins count too, so expect them to fire while you test.

No request ID

This is the one place these docs explain it; the other pages link here. A login or signup can arrive at your server without a requestId:

  • the browser's get() answered { capped: true }: a budget, a key's spend limit, the usage limit or the per-IP rate limit stopped it (Capped);
  • get() rejected: an ad blocker, a network failure or a setup mistake (error codes);
  • a bot posted the form straight to your server without running the agent.

Your server can't tell these apart, and a capped: true the browser claims proves nothing. So there's no score at all: challenge it like the middle band (a captcha, an emailed code or link, a short delay), and never let it through silently. Low-risk actions may go ahead. Treat a request ID whose event is too old or belongs to another user the same way.

Recipes

Starting points per action, to adjust to your traffic. Each assumes you've checked the event is fresh and belongs to this request, as in the quickstart.

Signups

  • Block: bot is bad (automation, headless, replayed or a fake crawler).
  • Challenge: socket-proof-failed; tls-mismatch; tampering.
  • Review: velocity['24h'].distinctExternalId of 3 or more (this visitor already made other accounts today), or GET /fingerprints?externalId= answers duplicate once you've attached the new id: the browser is shared with another account.
  • Ignore alone: vpn, incognito, privacy-browser.

Logins

  • New device: returning: false for a user you know, or a visitorId not among the visitors GET /fingerprints?externalId= lists for them: ask for a second factor.
  • Challenge: tor or proxy; a bad bot.
  • Credential stuffing: velocity['1h'].distinctExternalId above 2 (one browser trying many accounts), ipEvents far above your normal for one IP, or distinctCountry above 1 within an hour.
  • Account takeover: the user's own distinctVisitorIdByExternalId or distinctIpByExternalId jumping in 24 hours.

Checkout and payments

  • Challenge (3-D Secure, a manual review): tor, proxy or residential-proxy; vpn when the event's country isn't the card's or the shipping address's; timezone-mismatch with vpn.originTimezone far from the billing country; tampering.
  • Block: a bad bot.
  • Ignore: private-relay.

Free trials, coupons and referrals

  • Refuse the offer (not the account): returning: true on a signup, so this browser was here before; or duplicate from Related, so the browser is already tied to another account that had the offer.
  • Review: related external IDs at depth 2 on visitor-id, for rings that pass the trial around.
  • Private windows and cleared cookies don't help here: the visitor id usually stays the same.

Content scraping

  • Block: bot is bad. Allow: bot is good (a known crawler from its own network).
  • Rate limit: datacenter with high velocity['5m'].events or ipEvents.
  • Decide on ai-agent: let it read, or not.

AI agents

ai-agent marks a browser an AI drives on someone's behalf, like ChatGPT agent, Operator, Claude, Perplexity's Comet or Google's agents. It's set when the browser's or the request's user agent names an agent, or when the request is signed with Web Bot Auth, a standard way for an agent to sign its requests (the Signature-Agent and Signature-Input headers). The claim isn't checked against the agent's published keys, so a declared agent stays not_detected with a small weight; automation signals that show a script took the browser over make it bad.

No person is looking at the page, so decide per action: let an agent read, but maybe not sign up or pay.

Socket proof

While it collects, the agent opens a WebSocket to its endpoint's origin (wss://…/ws): a connection that stays open, which a browser makes easily and a simple script sending HTTP requests doesn't. It authenticates with the public key and a random id, and the API writes a random challenge to it. The agent sends the challenge back with the identification, so the socket has to read what the API wrote, not only connect. The event's socketProof says how it went:

socketProofMeansFlag
verifiedThe challenge came back.
wrong-challengeA challenge the API didn't write to this socket, one already used, or none.socket-proof-failed
connection-failedThe browser reports the socket couldn't connect.socket-proof-failed
challenge-not-receivedThe socket connected, but no challenge had arrived when the identification went out.socket-proof-failed
absentNo socket: the request didn't come from the agent, the browser has no WebSocket, or the check is off.socket-proof-failed while the check is on

A client that only sends HTTP requests, like a script replaying a captured identification, never gets there; a headless browser does, so this is evidence against plain clients, not against automation. The check is off by default, because proxies, firewalls and some networks block WebSockets for real visitors too. Turn it on when your visitors can open one: then every identification without a verified socket detects it. While it's off, the dashboard shows the check as Disabled.

The agent opens the socket to its endpoint's origin, the agent's own or your custom domain. A page whose Content Security Policy (the header that lists which servers a page may talk to) has connect-src needs the endpoint's WebSocket address in it (wss: and the endpoint's host; see the agent's CSP example), or the socket fails and no event gets the proof. Socket proof is turned on in the project's settings.

TLS fingerprint

Every HTTPS connection starts with a TLS handshake, in which the client lists the encryption it supports. Browsers each do it their own way, and http libraries theirs, whatever user agent they send. Our CDN reads the handshake of the connection that sends the identification as a JA4 fingerprint, like t13d1516h2_8daaf6152771_02713d6af862; every event keeps it as ja4, next to the older JA3 (ja3, a lowercase hex md5, when our CDN sends one) for tools that still use it. Only JA4 sets flags. Nothing about it comes from the browser.

tls-mismatch is set when the handshake doesn't fit the browser the user agent claims. The event's tlsMismatch says why: its reason, the claimedBrowser (like Chrome 156) and, for a known client, its name in client (like Go crypto/tls):

reasonMeans
non-browser-clientThe handshake belongs to a known client that isn't a browser, named in client: an http library (Go, curl, Python, Java, Node, Bun), a VPN client or known malware, whatever browser it claims.
old-tls-versionA recent Chrome, Edge, Opera, Firefox (100 and up) or Safari (15 and up) offered a TLS version below 1.3, which they never do.
no-http2Such a browser didn't offer HTTP/2: no ALPN, or HTTP/1.1 only, as many http libraries do.
rare-for-claimed-browserAcross every project, the claimed browser almost never makes this handshake while other browsers often do: Firefox's handshake from a browser that says it's Chrome. Learned from real traffic, so it needs a few hundred identifications of the browser first.

A network that intercepts TLS, like some company firewalls and antivirus programs, makes the handshake itself, so its users can get the signal too: weigh it with other signals. Every browser on iOS uses Safari's handshake, so Chrome on an iPhone is no mismatch.

Incognito

incognito is set when the browser is a private window. Some browser versions don't give it away, and then the event's incognito field is absent rather than false: no flag means not detected, not a normal window. The visitor ID doesn't depend on it, and the flag weighs little, since many people browse privately.

Privacy browsers

privacy-browser is set when Brave or DuckDuckGo's browser say what they are, or when Firefox's fingerprinting resistance shows (two cores and the UTC zone whatever the device), which Tor Browser and Mullvad Browser turn on. Many regular users pick these, so it adds nothing to the score. privacy-settings is separate: canvas, WebGL or audio reads come back randomized or replaced.

Rare devices

rare-device is set when the device's hardware (graphics card, cores, memory, platform) has been seen with at most one of the project's visitors, once the project has seen at least 10,000. Each project is counted on its own, so no project learns what hardware another one sees. It needs traffic to mean anything, and a rare device is often just a new model, so it weighs nothing.