Live: Tor + abuse feeds refreshed every 6 hours

LayerCall

Response values

Every field you can branch on, and every value it can take.

Two rules worth knowing before you write any of this. Read verdict if you want one decision and nothing else. Read actor.proven when the difference between evidence and proof matters — it is true for exactly one thing, a verified cryptographic signature, and false for every inference we make.

summary

/v1/score/user

One sentence describing the decision — the text for a Slack alert, a review ticket, or a log line someone greps in six months. Everything else in the response is the same decision in detail.

ValueMeansWhat to do
"Blocked (100/100) — Tor exit node, disposable domain and machine-generated handle."Verdict, score, and the strongest reasons in plain language.Show it to a person. It is built from the same top_signals as the rest of the response, so it can never disagree with them.

verdict

Every scoring endpoint

The only field most integrations need. Thresholds move with strictness (0–3); the score does not.

ValueMeansWhat to do
allowBelow the review threshold for your strictness.Let them through.
reviewEnough signal to be worth a second step, not enough to refuse.Step up — email or SMS verification, or a manual queue. Do not reject: this band is where false positives live.
blockPast the block threshold, or a hard rule fired.Refuse, or hold for manual approval if the cost of being wrong is high.

actor.type

/v1/score/user

What kind of thing this is, as opposed to what to do about it. Branch on actor.proven rather than on this list when you need certainty — it is true only for a verified signature.

ValueMeansWhat to do
verified_agentA Web Bot Auth signature verified against the operator's published key. Proven.Check actor.trigger: fetcher means a person asked for this and refusing refuses your customer. crawler means nobody did.
impersonated_agentA signature was presented and did not verify.Refuse. Nobody sends a broken Signature-Agent header by accident.
automationHeadless or automation markers in the browser, and unsigned. Inferred.Challenge rather than refuse outright — plenty of legitimate automation exists, and these markers can be patched by anything that cares.
likely_humanA device fingerprint with no automation markers. This is a statement about our evidence, not about your visitor.Treat as ordinary traffic. Do not treat it as proof a person is present — a capable AI agent driving a real browser reads identically.
unknownNo signature and no device fingerprint.Nothing to act on here. Serve /fp.js to get a device answer, or pass the agent's signed request.

actor.proven

/v1/score/user

The field to branch on when the difference between evidence and proof matters.

ValueMeansWhat to do
trueA cryptographic signature settled it. No false-positive rate.Safe to act on automatically, including allowing traffic you would otherwise challenge.
falseEverything else on the actor object is inference.Weigh it, do not obey it. Inference belongs in a challenge, not an automatic refusal.

agent.decision

/v1/agent/authorize and /v1/score/user

Your policy applied to this agent. Rules are evaluated in order, first match wins, and the defaults below apply when none match.

ValueMeansWhat to do
allowYour policy permits this agent to do this here.Serve the request.
denyYour policy refuses it. On /v1/score/user this also forces verdict to block.Refuse. You configured this — we are not overruling you by reporting allow.
reviewNo rule matched and the defaults could not decide — typically a verified agent that publishes no card.Fall back to your normal risk handling; the score is still there.

agent.matched

/v1/agent/authorize and /v1/score/user

Which rule decided. rule[N] is your own policy at that index; the rest are the built-in defaults, which apply only when no rule of yours matched.

ValueMeansWhat to do
rule[N]Your policy rule at index N matched.Nothing — this is your own configuration working.
default:signature_failedA signature was offered and did not verify.Denied. Strictly worse than sending no signature at all.
default:unsignedNo Web Bot Auth signature present. Most traffic.Review. Identity is not a question this can answer — use the risk score.
default:verified_fetcherVerified, and the operator declares a person initiated it.Allowed, including on POST. Refusing this refuses the customer who asked for it.
default:crawler_state_changeVerified autonomous crawler attempting POST/PUT/PATCH/DELETE.Denied. An agent acting on nobody's behalf has no business signing up or checking out.
default:verified_crawler_readVerified autonomous crawler making a safe request.Allowed.
default:verified_no_cardVerified cryptographically, publishes no Signature Agent Card.Review. We know who, not what. Currently the common case.

status

/v1/verify/email

ZeroBounce-compatible vocabulary, so existing branching ports across unchanged.

ValueMeansWhat to do
validNothing disqualifying found — the syntax, the domain and the mail servers all check out. It does NOT mean the specific mailbox exists: that check is not enabled yet, which is what sub_status: mailbox_not_verified is telling you.Accept for a signup. For a mailing list, treat it as 'the domain can receive mail', not 'this person can'.
invalidStructurally bad, no MX, or the mailbox was verified not to exist.Reject and ask for a correction — check did_you_mean first.
riskyDeliverable but carries risk: catch-all domain, suspicious handle.Accept with verification rather than rejecting.
do_not_mailDisposable or a role account.Reject for signup; never add to a mailing list.

sub_status

/v1/verify/email

Why the status is what it is. Free-form, and stable — new values may be added.

ValueMeansWhat to do
failed_syntax_checkNot a valid address.Reject; ask again.
disposableA throwaway provider.Reject for signup.
role_basedA shared inbox (info@, support@).Fine for B2B contact, poor for an individual account.
suspicious_handleThe local part looks machine-generated.Verify rather than reject — real people do have odd addresses.
catch_allThe domain accepts mail for addresses that do not exist.Nobody can verify this mailbox. Do not treat acceptance as evidence.
mailbox_verifiedThe mail server confirmed the mailbox exists.Strongest deliverability signal available.
mailbox_not_foundThe mail server said it does not exist.Reject.
mailbox_not_verifiedThis provider does not give a trustworthy answer to a mailbox probe, so we do not claim one. Not the same as invalid.Judge on the other signals — everything except the mailbox itself was checked.
newly_registered_domainThe domain was registered fewer than 30 days ago.Strong fraud signal in combination; weak alone.
risky_tldA TLD with disproportionate abuse.Contributing signal, never a reason on its own.
possible_typoLooks like a misspelling of a common domain.Show did_you_mean and let the user correct it.
community_reported_fraudAnother customer reported this value as confirmed fraud.Treat seriously — this is a real report, not an inference.

signals.mailbox_status

/v1/verify/email

Which kind of 'unknown' you have. A bare null cannot distinguish 'nobody can ever verify this' from 'we have not got to it yet', and those call for opposite reactions.

ValueMeansWhat to do
verifiedA real yes/no from the mail server.Trust mailbox_exists.
catch_allThe domain accepts everything. Unverifiable by anyone, at any price.Stop asking. Roughly a quarter of sampled domains, concentrated in B2B SaaS.
pendingQueued; the answer will be there on the next lookup.Proceed on the other signals, or pass ?wait_for_mailbox=true to block on it.
unsupportedWe deliberately do not probe this domain.Judge on the other signals.
unavailableNo probe host is configured.Same as unsupported.
unknownThe request's own time budget ran out before the mailbox stage was reached, so nothing was attempted.Retry, or pass ?wait_for_mailbox=true. Unlike unsupported and catch_all, this one can succeed next time.

parse_status

/v1/lookup/phone

Why a number did not parse — and, in one case, how to make it parse. This file opens by promising every value you can branch on, exhaustively, and warning that an unlisted value falls through to a default and gets treated as allow. parse_status appeared here zero times and nowhere on the docs site, so a customer validating numbers entered without a country code got country_required, did not recognise it, fell through to their default branch, and let the number through unvalidated. They believed they were checking phones.

ValueMeansWhat to do
okParsed and assigned to a real numbering plan.Read the rest of the response.
country_requiredA plausible run of digits with no country code. We cannot tell +1 415 555 2671 from +44 7911 123456 without one.Resend with &country=XX (ISO-3166 alpha-2) and it will work. This is the one parse_status a retry fixes.
impossibleNot a phone number in any plan on earth — too few digits, or a country code that was tried and rejected.Reject it. A retry with a country cannot help.
invalid_patternA plausible length and prefix, but not an assigned pattern in that country.Reject it, or ask the person to check what they typed.

Signal flags

Every boolean in a signals object. Most are true-is-bad, but not all — the ones marked TRUE IS GOOD are health flags whose absence is the risk, and getting that backwards is the most common way an integration ends up rejecting good users.

A null anywhere means we could not determine it. It never means false, and it should never count against a user.

IP /v1/score/ip

  • is_vpnCommercial VPN egress. Review, not block — plenty of legitimate users.
  • is_proxyAnonymising egress. Derived from is_tor or is_vpn, so it adds no new information.
  • is_datacenterHosting provider rather than a consumer connection.
  • is_torA Tor exit node.
  • recent_abuseAbuse history from our own reputation network.
  • is_hijacked_netblockInside a block Spamhaus DROP lists as hijacked or criminal-controlled. Attribution in hijacked_source.
  • is_private_or_reservedRFC1918 or reserved space — not routable, usually a misconfiguration on your side.

Email /v1/verify/email

  • syntax_validParses as an address. Unicode local parts are accepted (RFC 6531).
  • mx_foundThe domain publishes MX records.
  • is_disposableA known throwaway provider.
  • is_homographMixed-script domain designed to look like another.
  • is_role_accountShared inbox rather than a person.
  • is_free_providerGmail, Outlook and similar. Ambiguous on its own.
  • is_suspicious_handleThe local part looks machine-generated.
  • is_taggedUses a +tag. Often legitimate; useful for dedupe via normalized_email.
  • is_risky_tldA TLD with disproportionate abuse.
  • is_new_domainRegistered fewer than 30 days ago, via RDAP. null means the registry could not be reached — an unknown age, not an old domain.
  • has_spf / has_dmarcEmail authentication posture. Absence is the risk signal.
  • has_websiteThe domain serves a site. null means undetermined, never counted against it.
  • mailbox_existsSMTP confirmation. null when unverified — see mailbox_status.
  • is_catch_allThe domain accepts mail for addresses that do not exist.
  • has_digital_footprintPublic Gravatar or breach history — an aged real identity lowers risk.

Phone /v1/lookup/phone

  • syntax_validParses for its country.
  • is_possibleMatches a possible length. TRUE IS GOOD — this is a health flag despite the name.
  • assigned_area_codeThe area code is actually allocated. TRUE IS GOOD.
  • is_voipVoIP range. Available for GB, FR, NL, IT, CA and others — not for +1 US numbers, whose plan has no VoIP range.
  • is_premium_ratePremium-rate number.
  • is_toll_freeToll-free number. Ambiguous rather than bad.
  • is_fictionalA reserved or film-use number (555 ranges).

Domain /v1/score/domain

  • resolvesDNS resolves. TRUE IS GOOD.
  • mx_foundCan receive mail. TRUE IS GOOD.
  • has_spf / has_dmarcAuthentication posture. TRUE IS GOOD.
  • is_disposableA known throwaway domain.
  • is_homographMixed-script lookalike.
  • is_free_providerA consumer mail provider rather than an owned domain.
  • is_risky_tldHigh-abuse TLD.
  • newly_registeredRegistered fewer than 30 days ago. null means the registry could not be reached — an unknown age, not an old domain.

Device /v1/score/device

  • is_botbot_probability reached 60 or more. That number is the sum of what was actually observed: injected automation globals (80), navigator.webdriver (70), a headless browser (60), a user agent that names a scraping tool (55), a zero-sized screen (30), no plugins (10), no languages (10).
  • is_automatedAn automation framework was detected (webdriver and similar).
  • is_headlessA headless browser.
  • timezone_mismatchBrowser timezone disagrees with the IP's location. null = the country could not be resolved, so no comparison was made — treat it as unknown, not as agreement.
  • repeat_deviceSeen before. Ambiguous — a returning customer looks the same as a trial cycler until you check linkage.

Linkage /v1/score/user

  • device_email_countDistinct emails seen with this device. The strongest multi-account signal here.
  • email_device_countDistinct devices seen with this email.
  • email_ip_countDistinct addresses seen with this email.
  • subnet_rate_1hLookups from this /24 in the last hour. Scoped to the subnet because rotating inside one is the cheapest evasion there is.
  • domain_rate_1hLookups against this email domain in the last hour. Free providers are exempt.

The numbers

Every numeric field, and which end is the bad end. Two of these run in opposite directions inside the same response, so it is worth reading the direction line before writing a comparison.

billable_lookups every scored response, including test mode and cache hits

Range
whole numbers. Usually 1. /v1/score/user bills one per component, so up to 4.
Direction
This is the number you are charged for — not the number of fields returned, and not the number of feeds consulted.
What to do
Sum it to reconcile your own bill against ours. Two cases surprise people and both are deliberate: a cached answer bills 0, and a test-mode call bills 0 while still counting against the test allowance. If it is 0 you were not charged.

processing_time_sec every scored response

Range
seconds, three decimal places. Typically 0.2–0.6 warm; a cold instance can be several seconds.
Direction
Our server time only. It excludes the network both ways, so it will always read lower than what your own client measures.
What to do
Use it to tell 'the API was slow' apart from 'the connection was slow' before raising a ticket. Do not use it as an SLA figure — the number your users feel is the round trip, not this.

signals_unavailable any scored response, ABSENT when everything answered — never an empty array

Range
an array of source names: tor, ip_reputation, vpn_providers, cloud_ranges, hijacked_ranges, disposable_domains, dns, geo, abuse_reports, gravatar, domain_age, reputation_network, custom_rules, digital_footprint.
Direction
Names what was NOT measured. A clean answer carrying a name is weaker evidence than the same answer without it — nothing is being asserted about those sources, in either direction.
What to do
READ THE NAMES, do not test the key. Two different facts arrive here: a source that was down, which clears, and a source that does not cover the input you sent, which never does. Every IPv6 address permanently carries ip_reputation, cloud_ranges, hijacked_ranges, abuse_reports and vpn_providers, because those feeds are IPv4-only — and mobile carriers are heavily IPv6, so `if (res.signals_unavailable) review()` sends a large and permanent share of real signups to manual review. Branch on the name you actually depend on: screening for Tor, check whether `tor` is in the list; if it is not, the Tor check ran.

request_id every response, including errors

Range
a string, `req_` followed by a UUID
Direction
Not a value to branch on — a value to KEEP. It is the one thing that lets us find your exact call in our logs.
What to do
Log it alongside your own record of the decision. Quoting it in a support ticket is the difference between us answering in minutes and asking you for a reproduction.

risk_score every scoring endpoint

Range
0–100, whole numbers
Direction
HIGH is bad. 100 is the worst.
What to do
Prefer `verdict` — it already applies the thresholds, and it moves with the strictness setting. Branch on the number only when you want your own cut-offs.

bot_probability /v1/score/device, and the device component of /v1/score/user

Range
0–100, whole numbers. The same scale in test mode and in production.
Direction
HIGH means more likely automated. It is NOT a risk score — plenty of automated traffic is legitimate, which is why it is reported separately.
What to do
60 or more is what sets is_bot, and is a reasonable place to start. 95 or more is a stacked, unambiguous automation signature: injected globals plus webdriver, or a headless browser naming itself. Below 55 no single automation marker has fired at all.

deliverability_score /v1/verify/email

Range
0–100, whole numbers
Direction
LOW is bad — the OPPOSITE of risk_score, which sits beside it in the same response and also ends in _score. 0 means do not send.
What to do
This is a mail-sending judgement, not a fraud judgement: it answers 'will this reach an inbox', while risk_score answers 'is this signup safe'. They disagree often and correctly. Note that mailbox verification is not yet enabled, so a syntactically valid address at a live domain scores 90 whether or not the mailbox exists — sub_status says mailbox_not_verified when that is the case.

abuse_reports every scoring endpoint

Range
0 upward
Direction
HIGH is bad. Confirmed reports from the network, not guesses.
What to do
Any non-zero value is worth reading. It is already folded into risk_score; the raw count is there so you can weight it yourself.

domain_age_days /v1/score/domain, /v1/verify/email

Range
0 upward, or null
Direction
LOW is bad. Under 30 days is what sets newly_registered.
What to do
null means the registry could not be reached, or publishes no RDAP service for that extension — an UNKNOWN age, never an old domain. Coverage is UNEVEN and worth planning for: measured across 36 real domains, every .com returned a real age while 8 returned null, spread across .co.jp, .us, .so, .me, .de, .ru and .it. Roughly a fifth. Domain age is one of the strongest fraud signals there is, so on those extensions a brand-new domain gets none of the newness penalty a .com would — do not read null as reassurance.

times_seen every scoring endpoint

Range
1 upward
Direction
Neither, on its own. A returning customer and a script look identical here.
What to do
Read it with first_seen and the linkage counts. Forty sightings in an hour is a script; forty over a year is a customer.

A starting policy

If you want something to paste and adjust rather than a menu, this is a defensible default. It refuses almost nothing outright, because the expensive mistake in this product is refusing a real customer.

const r = await score(signup);

// 1. A denied agent is your own policy. Obey it.
if (r.agent?.decision === "deny") return reject(r.agent.reason);

// 2. Proof outranks inference. A verified user-initiated agent
//    is a customer's assistant doing what they asked.
if (r.actor.proven && r.actor.type === "verified_agent"
    && r.actor.trigger === "fetcher") return allow();

// 3. Disposable addresses and confirmed fraud: refuse.
if (r.components.email?.status === "do_not_mail") return reject();
if (r.components.email?.sub_status === "community_reported_fraud") return reject();

// 4. One machine across many identities is the clearest
//    multi-account pattern you will get.
if ((r.linkage.device_email_count ?? 0) >= 5) return stepUp();

// 5. Otherwise let the verdict decide.
if (r.verdict === "block") return reject();
if (r.verdict === "review") return stepUp();   // verify, don't refuse
return allow();

Note what is not in it: nothing refuses on is_vpn, is_datacenter or automation alone. Each is a contributing signal with a real false-positive rate, and blocking on any one of them is the most common way to lose exactly the technical users you want.

Once you are live, send outcomes back to /v1/outcome. It is free, and it is the only way either of us finds out whether these thresholds are right for your traffic.