Live: Tor + abuse feeds refreshed every 6 hours

LayerCall

API reference

Every endpoint returns the same shape
8 endpoints · one key · one verdict

Start here

Which endpoint do you want?

Start from what you already have in your hand.

You haveCall
a whole signup — any mix of email, IP, phone, domain or deviceGET/POST /v1/score/user
an IP addressGET /v1/score/ip
an email addressGET /v1/verify/email
a phone numberGET /v1/lookup/phone
a domain nameGET /v1/score/domain
a device fingerprintPOST /v1/score/device
a request that claims to be from an AI agentPOST /v1/verify/agent
an AI agent asking to do somethingPOST /v1/agent/authorize

Not sure, or you have several of them? Send whatever you have to /v1/score/user and it scores them together in one call.

GET/POST

Unified Score — score a whole signup

GET/POST/v1/score/user

The endpoint the others feed into: pass any combination of ip, email, phone (+ optional phone_country), and domain. Components run in parallel and are combined into one weighted score — a hard block on any single component floors the combined score at review, so one definitive red flag is never averaged away by four clean ones — it is not promoted to an outright block either, because one component being certain is not the same as the signup being certain, and refusing a real customer is the expensive mistake here. Raise strictness to 2 if you want that floor to land on block instead. Billed as one credit per component checked.

The response also carries linkage — the pattern no single value can show you. Forty signups from forty clean addresses with forty plausible inboxes each score allow, correctly, because each one really is unremarkable; what gives the operator away is device_email_count, email_device_count, email_ip_count and subnet_rate_1h. Velocity is scoped to the /24 rather than the address, because rotating inside a subnet is the cheapest evasion there is. A null means we do not know, never zero. These are reported today and do not yet move the score — we publish constants after measuring them, not before.

The response also says what kind of thing it is. A verdict tells you what to do; it does not tell you whether you just refused a scraper, somebody’s assistant acting on their instruction, or a person on a VPN. The actor block answers that, with the one field that matters most: proven.

"actor": {
  "type":     "verified_agent",     // verified_agent | impersonated_agent |
                                    // automation | likely_human | unknown
  "proven":   true,                 // true ONLY for a verified signature
  "basis":    "signature",          // signature | device_signals | none
  "operator": "chatgpt.com",
  "trigger":  "fetcher",
  "detail":   "… verified, and declares this request was initiated by a person."
}

proven is true for exactly one thing: a Web Bot Auth signature that verified. That is arithmetic and has no false-positive rate. Everything else — headless detection, automation markers — is inference, and a capable adversary patches those in an afternoon. Branch on proven rather than reading the vocabulary, and you will never treat a guess as a certainty.

There is deliberately no human value. The premise of this whole layer is that a capable AI agent passes every human check — real browser, real fingerprint, real timezone — so nothing here can establish that a person is present. likely_human means “nothing here looks automated”, which is a statement about our evidence rather than about your visitor, and the hedge is in the name so it cannot be dropped on the way into your database.

AI agent authorization is part of this call too. Pass the signed request the agent made to you as agent and the response gains an agent block with the decision, which rule made it, and why. The signature covers the agent’s method, URL and headers, so those cannot be inferred from your call to us — send them as they arrived. Omit the field entirely for an ordinary signup and nothing changes.

curl -X POST "https://www.layercall.com/v1/score/user" \
  -H "X-Api-Key: tl_live_your_key" -H "Content-Type: application/json" \
  -d '{
    "ip": "$IP", "email": "$EMAIL", "device_id": "$DEVICE_ID",
    "agent": {
      "method": "POST",
      "url": "https://yoursite.com/signup",
      "headers": { "signature": "…", "signature-input": "…", "signature-agent": "…" }
    }
  }'

The decision is reported beside the score, never averaged into it — “may this agent do this” and “how risky is this identity” are different questions. The one exception is a deny, which forces the verdict to block: you configured that policy, and returning allow while your own rule says otherwise would be overruling you. Prefer /v1/agent/authorize when you are gating traffic that has no signup identity attached.

curl -X POST "https://www.layercall.com/v1/score/user" \
  -H "X-Api-Key: tl_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "ip": "146.70.202.5",
    "email": "ceo@mailinator.com",
    "phone": "+14155552671",
    "domain": "just-registered-shop.com"
  }'
{
  "risk_score": 71,
  "verdict": "block",
  "components_checked": ["ip", "email", "phone", "domain"],
  "top_signals": [
    "ip: commercial vpn",
    "email: disposable domain",
    "domain: registered <30 days ago"
  ],
  "components": {
    "ip":     { "risk_score": 55, "verdict": "review", "...": "full detail" },
    "email":  { "risk_score": 70, "verdict": "block",  "...": "full detail" },
    "phone":  { "risk_score": 0,  "verdict": "allow",  "...": "full detail" },
    "domain": { "risk_score": 60, "verdict": "review", "...": "full detail" }
  },
  "request_id": "req_9f2c1b7e-4a55-4c2e-9d31-7b0f6a81c4d2",
  "processing_time_sec": 0.052
}

POST

Outcome — tell us whether we were right

POST/v1/outcome

Every score comes back with a request_id. Quote it here once you know what actually happened, and the engine learns from it. This is the only mechanism by which our accuracy improves other than by hand, which is why it is free — charging you to improve the product you already bought would be absurd.

Two things happen. Your own calibration gets a data point, so “how often was the block verdict right for us” becomes answerable. And a fraud label raises the abuse counters on every value in that score across the whole network — so your confirmed loss protects the next customer immediately. That reciprocity is the entire reason the reputation network is worth anything.

curl -X POST "https://www.layercall.com/v1/outcome" \
  -H "X-Api-Key: tl_live_your_key" -H "Content-Type: application/json" \
  -d '{ "request_id": "req_5fddd4b0-…", "outcome": "fraud" }'

# Reconciling a day of chargebacks? Send an array, up to 500.
{
  "recorded": 1,
  "not_found": 0,
  "results": [
    {
      "request_id": "req_5fddd4b0-…",
      "status": "recorded",
      "our_score": 32,
      "our_verdict": "allow",
      "disagreement": true
    }
  ]
}

disagreement is the field worth alerting on: it is true when we said allow and you saw fraud, or we said block and the signup was legitimate. Those are the rows that should drive your next strictness change. outcome is fraud or legitimate. Relabelling is allowed — a signup can look fine for a week and then charge back — and scores are retained 180 days, which is past the chargeback window. Test keys are refused: a fraud label writes to the live shared network.

POST

Batch — up to 500 lookups in one request

POST/v1/batch

One request, one type, up to 500 values. Billed identically to 500 single calls — there is no bulk discount hidden in the plumbing, and equally no penalty. Use it for list cleaning and backfills rather than looping the single-record endpoints and managing your own concurrency.

curl -X POST "https://www.layercall.com/v1/batch" \
  -H "X-Api-Key: tl_live_your_key" -H "Content-Type: application/json" \
  -d '{ "type": "email", "items": ["a@example.com", "b@example.com"] }'

# type is one of: ip, email, phone, domain

Each item succeeds or fails on its own — one unparseable value in 500 does not fail the request, it comes back as a result with an error on it. That is the reason to use this endpoint rather than a loop, so it is worth seeing before you write the code that reads it:

{
  "type": "email",
  "count": 2,
  "succeeded": 1,
  "failed": 1,
  "billable_lookups": 1,      // failures are not charged
  "results": [
    {
      "input": "a@example.com",
      "risk_score": 100,
      "verdict": "block",
      "status": "invalid",
      "sub_status": "reserved_domain",
      "signals": { "syntax_valid": true, "mx_found": false, ... },
      "request_id": "req_..."
    },
    {
      "input": "not-an-email",
      "error": "Not a valid email address."
    }
  ]
}

results is in the same order as items, and every entry carries back the input it came from, so you can join on either. A failed item has error and nothing else.

GET

IP Intelligence

GET/v1/score/ip

VPN, proxy, datacenter, Tor, and abuse signals with geo/ASN context. VPN/datacenter detection runs per-IP across 50k+ ranges (not just ASNs), so it catches a VPN running on a cloud provider. Egress severity: Tor > VPN > datacenter (strongest tier counts, not summed), with abuse and our own network reports stacking on top. Cached lookups return in about 0.06 sec.

is_hijacked_netblock is a separate dimension from all of that, and it exists because we measured its absence: forty addresses from Spamhaus’s do-not-route list scored 0/allow, because hijacked space is not a VPN, not a datacenter and not Tor — looking like nothing in particular is the entire reason it gets stolen. It stacks rather than replacing, since stolen space says nothing about whether the traffic is also anonymised. When it fires, hijacked_source names where the claim came from, because using that data is conditional on crediting it.

curl "https://www.layercall.com/v1/score/ip?ip=185.220.101.1" \
  -H "X-Api-Key: tl_live_your_key"

GET

Email Validation

GET/v1/verify/email

Industry-standard status / sub_status vocabulary (valid, invalid, risky, do_not_mail — drop-in compatible with ZeroBounce-style branching), plus: MX and provider verification, 4,000+ disposable domains refreshed daily, did_you_mean typo correction that also catches typo-squat domains like gamil.com, normalized_email for multi-account dedupe (strips +tags and Gmail dots), machine-generated handle detection, domain age via RDAP, high-abuse TLD flags, first_seen / times_seen velocity from our own network, and a digital_footprint object (public Gravatar profile + optional breach history — an aged real identity lowers risk). When the mailbox can't be verified we say so (mailbox_not_verified) rather than guessing.

mailbox_status says which kind of “unknown” you have, because a bare null cannot distinguish them and they call for opposite reactions: verified (a real answer), catch_all (the domain accepts mail for addresses that do not exist, so nobody can ever verify it — measured on 26% of sampled domains, concentrated in B2B SaaS), pending (queued; the answer will be there next time), unsupported, unavailable, or unknown (the request’s time budget ran out before the mailbox stage was reached). Add ?wait_for_mailbox=true to block on the probe instead — seconds, not milliseconds, which is what every real-time verification API actually costs.

curl "https://www.layercall.com/v1/verify/email?email=ceo@mailinator.com" \
  -H "X-Api-Key: tl_live_your_key"
{
  "email": "test@guerrillamail.com",
  "normalized_email": "test@guerrillamail.com",
  "risk_score": 100,
  "verdict": "block",
  "status": "do_not_mail",
  "sub_status": "disposable",
  "deliverability_score": 0,
  "did_you_mean": null,
  "signals": {
    "syntax_valid": true,
    "mx_found": true,
    "is_disposable": true,
    "is_homograph": false,
    "is_role_account": true,
    "is_free_provider": false,
    "is_suspicious_handle": true,
    "is_tagged": false,
    "is_risky_tld": false,
    "is_new_domain": null,
    "has_spf": true,
    "has_dmarc": true,
    "has_website": true,
    "mailbox_exists": null,
    "is_catch_all": null,
    "mailbox_status": "unavailable",
    "has_digital_footprint": null
  },
  "domain": "guerrillamail.com",
  "domain_age_days": null,
  "mx_provider": null,
  "mx_records": [
    "mail.guerrillamail.com."
  ],
  "abuse_reports": 0,
  "digital_footprint": {
    "has_gravatar": false,
    "gravatar_profile_url": null,
    "breach_count": null,
    "seen_in_breach": null
  }
}

GET

Phone Validation

GET/v1/lookup/phone

Validity, country, E.164/national/international formats, and line type for 240+ countries — with premium-rate risk flags. VoIP detection comes from national numbering-plan ranges, so it is available for GB, FR, NL, IT, CA and others, but not for +1 US numbers (the NANP plan has no VoIP range). Accepts E.164 directly or national format with &country=.

The parameter is country here and phone_country on /v1/score/user, because that endpoint takes several values at once and a bare country would be ambiguous. Unknown parameters are ignored rather than refused, so sending the wrong one gets no error — a national-format number simply parses as though no country were given. If parse_status comes back country_required, that is what happened.

curl "https://www.layercall.com/v1/lookup/phone?phone=%2B14155552671" \
  -H "X-Api-Key: tl_live_your_key"
# national format also works: ?phone=4155552671&country=US
{
  "parse_status": "ok",
  "phone": "+14155552671",
  "risk_score": 0,
  "signals": {
    "syntax_valid": true,
    "is_possible": true,
    "is_voip": false,
    "is_premium_rate": false,
    "is_toll_free": false,
    "assigned_area_code": true,
    "is_fictional": false
  },
  "number": {
    "e164": "+14155552671",
    "country": "US",
    "national": "(415) 555-2671",
    "international": "+1 415 555 2671",
    "line_type": "fixed_line_or_mobile"
  },
  "verdict": "allow",
  "abuse_reports": 0
}

GET

Domain Intelligence

GET/v1/score/domain

Registration age via RDAP (newly-registered domains are a strong fraud signal), DNS resolution, MX/SPF/DMARC email posture, and disposable-domain detection.

curl "https://www.layercall.com/v1/score/domain?domain=stripe.com" \
  -H "X-Api-Key: tl_live_your_key"
{
  "domain": "github.com",
  "risk_score": 0,
  "verdict": "allow",
  "signals": {
    "resolves": true,
    "mx_found": true,
    "has_spf": true,
    "has_dmarc": true,
    "is_disposable": false,
    "is_homograph": false,
    "is_free_provider": false,
    "is_risky_tld": false,
    "newly_registered": false
  },
  "registration": {
    "created_at": "2007-10-09T18:20:50Z",
    "age_days": 6893,
    "registrar": "MarkMonitor Inc."
  },
  "abuse_reports": 0
}

POST

Device Fingerprint — the signal that survives a VPN

POST/v1/score/device

A fraudster you block switches to a VPN and a disposable inbox — both free, both already detected above. What they don’t change is the machine in front of them. Drop fp.js on your signup page to get a stable device_id, then score it. No cookie is set and nothing is written to the device, so clearing storage or opening incognito returns the same id — which is the entire point.

<!-- 1. On your signup page -->
<script src="https://www.layercall.com/fp.js"></script>
<script>
  LayerCall.fingerprint().then(fp => {
    // POST fp.device_id (and optionally fp.signals / fp.automation)
    // to YOUR backend along with the rest of the form
  });
</script>
# 2. From your backend
curl -X POST "https://www.layercall.com/v1/score/device" \
  -H "X-Api-Key: tl_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"device_id":"ff48aa64a243c1b9e07d5c2f8a1b4e6d","ip":"185.220.101.5","automation":{"webdriver":false}}'
{
  "device_id": "ff48aa64a243c1b9e07d5c2f8a1b4e6d",
  "risk_score": 45,
  "verdict": "review",
  "bot_probability": 0,
  "signals": {
    "is_bot": false,
    "is_automated": false,
    "is_headless": false,
    "timezone_mismatch": true,
    "repeat_device": true
  },
  "first_seen": "2026-07-02T09:14:22Z",
  "times_seen": 38,
  "abuse_reports": 1,
  "processing_time_sec": 0.004
}

bot_probability is separate from risk_score on purpose: “this is a script” and “this is dangerous” are different questions, and plenty of automated traffic is perfectly legitimate. Automation markers are collected but deliberately not hashed into the id, so a bot that patches navigator.webdriver cannot also shed the reputation it has built up.

You can also pass device_id straight to /v1/score/user to fold it into a whole-signup score. Privacy: the id is hashed in the browser and hashed again before storage, so the stored value cannot be reversed to a device — not even by us. Raw browser characteristics are used for bot scoring only and are never persisted.

GET/POST/DELETE

Custom Rules — your lists always win

GET/POST/DELETE/v1/rules

Exclude your own infrastructure or block known bad actors: add allow or block rules for any ip, cidr range, email, domain (subdomains included), phone, or asn. Rules override the computed score on every endpoint — an allow rule forces 0/allow, a block rule forces 100/block, and the response shows rule_applied so you always know why. Block rules take precedence over allow rules. Kind is auto-detected per value. Rule management calls are free.

# Never flag our office network or corporate domain
curl -X POST "https://www.layercall.com/v1/rules" \
  -H "X-Api-Key: tl_live_your_key" -H "Content-Type: application/json" \
  -d '{ "action": "allow", "values": ["203.0.113.0/24", "ourcompany.com"] }'

# List, delete one, or clear all
curl "https://www.layercall.com/v1/rules" -H "X-Api-Key: …"
curl -X DELETE "https://www.layercall.com/v1/rules/<rule-id>" -H "X-Api-Key: …"
curl -X DELETE "https://www.layercall.com/v1/rules?confirm=true" -H "X-Api-Key: …"

POST

Report Fraud — grow the shared network

POST/v1/report

Caught a fraudster? Report the IP, email, domain, or phone and it feeds our reputation network — so the next time anyone (including you) scores that value, it carries the abuse history. This is our own data, built from reports like yours: no third-party licenses, no stale lists. Reported values raise abuse_reports and recent_abuse on every future score. One report counts as a single lookup against your plan, however many values it carries — reporting is what makes the shared data worth having, so it is not priced to discourage.

curl -X POST "https://www.layercall.com/v1/report" \
  -H "X-Api-Key: tl_live_your_key" -H "Content-Type: application/json" \
  -d '{ "values": ["45.9.20.14", "scammer@example.com"] }'

# → { "reported": 2, "results": [ { "kind": "ip", "abuse_reports": 7 }, … ] }

POST

Migration Import — switch providers in one call

POST/v1/rules/import

Switching from another provider? Export your allowlist/blocklist there and POST the file here as-is — plain text or CSV, one entry per line. We auto-detect IPs, CIDR ranges, emails, domains, phones, and ASNs, skip headers and junk columns, dedupe, and report exactly what was imported and what was skipped. Every line is accounted for: imported + duplicates (already on your account) + duplicates_in_file (repeated in your upload) + skipped. Up to 10,000 entries per call, free.

curl -X POST "https://www.layercall.com/v1/rules/import?action=block" \
  -H "X-Api-Key: tl_live_your_key" \
  -H "Content-Type: text/csv" \
  --data-binary @old-provider-blocklist.csv
{
  "action": "block",
  "imported": 4210,
  "duplicates": 12,
  "duplicates_in_file": 87,
  "by_kind": { "ip": 3900, "cidr": 180, "domain": 130 },
  "skipped": [ { "line": 1, "value": "ip_address,date_added", "reason": "no ip/cidr/email/domain/phone/asn recognized" } ],
  "processing_time_sec": 0.19
}