layercall
Official Python client for LayerCall — score an IP, email, phone number, domain, or a whole signup for fraud in a single call.
Zero dependencies. Standard library only. A trust check sits on your signup path, which is the worst place to add a dependency tree that has to resolve against whatever your app already pins.
pip install layercall
Quick start
import os
from layercall import LayerCall
lc = LayerCall(os.environ["LAYERCALL_API_KEY"])
result = lc.score_user(ip=request.remote_addr, email=form["email"])
if result["verdict"] == "review":
send_otp(form["email"]) # a real user clears it themselves
elif result["verdict"] == "block":
queue_for_review(result)
Get a free API key — 1,000 lookups a month, no card.
Acting on a verdict
| Verdict | Do this | Not this |
|---|---|---|
allow |
Let them through | — |
review |
Step up — OTP, SMS, 3-D Secure | Don't reject. This band includes ordinary VPN users. |
block |
Reject, or send to a human queue | Don't reject silently |
Prefer a challenge over a rejection. A real user clears it in seconds; an attacker cannot. A false positive then costs friction instead of a customer.
Already send an OTP to everyone? Passwordless products can't use an email code as a step-up — it's already mandatory. Score after sign-in and use a different lever: a limited account state, a delayed payout, or a review queue.
Methods
lc.score_ip("185.220.101.1")
lc.verify_email("someone@mailinator.com")
lc.lookup_phone("+14155552671")
lc.lookup_phone("4155552671", country="US")
lc.score_domain("example.com")
# score_device is the one method you cannot try without a browser: it needs a
# real fingerprint and the API correctly refuses anything else. On a TEST key
# these two fixtures work verbatim, so you can call it today and wire up
# /fp.js afterwards.
lc.score_device("0" * 32) # clean browser
lc.score_device("f" * 32) # headless automation
lc.score_device(device_id, ip=ip, signals=signals) # real: fingerprint from /fp.js
lc.score_user(ip=ip, email=email, phone=phone, device_id=device_id)
lc.batch("email", ["a@x.com", "b@y.com"]) # up to 500
# AI agents — proof of identity, then your policy applied to it
lc.verify_agent(url=url, headers=headers) # who is this?
lc.authorize_agent(method=method, url=url, headers=headers) # may they, here?
lc.get_agent_policy()
lc.set_agent_policy([{"trigger": "crawler", "path": "/api", "action": "deny"}])
# Tell us whether a score was right. Free, and the only thing that improves it.
lc.report_outcome(request_id=r["request_id"], outcome="fraud")
# Report confirmed fraud to the shared reputation network. Live keys only.
lc.report("ip", "185.220.101.1", reason="carding")
Custom rules - your lists always win
A rule overrides the computed score for that value. The kind (ip, cidr,
email, domain, phone, asn) is detected from the value unless you force
it.
lc.add_rules("block", ["185.220.101.1", "mailinator.com"])
lc.add_rules("allow", "10.0.0.0/8")
lc.add_rules("block", ["AS14061"], kind="asn")
listed = lc.list_rules()
lc.delete_rule(listed["rules"][0]["id"])
# Paste a list - one value per line, or CSV.
lc.import_rules("block", "1.2.3.4\n5.6.7.8\nspam.example")
# Removes EVERY rule on the account. confirm is required, deliberately.
lc.clear_rules(confirm=True)
A pending mailbox
verify_email returns mailbox_status "pending" when the SMTP probe has not
finished - it is queued and the answer is there on the next lookup. If you
would rather wait for it, say so. It costs 2-10 seconds on a cache miss, which
is why it is opt-in:
r = lc.verify_email("someone@example.com", wait_for_mailbox=True)
That is all thirteen. This block used to list seven, with no hint there were more — so a Python developer reasonably concluded that device scoring, agent authorization and outcome feedback were Node-only features. They were not; they were shipped, working and undocumented.
Every scoring method takes strictness (0 lenient → 3 paranoid) —
score_ip, verify_email, lookup_phone, score_domain, score_device,
score_user and batch. It moves the verdict thresholds only; the risk score
never changes, so you can re-tune without re-scoring anything.
The other methods report or configure rather than score, and take no
strictness: report, report_outcome, verify_agent, authorize_agent,
get_agent_policy, set_agent_policy.
None means unknown, never "no"
signals = lc.score_domain("example.de")["signals"]
signals["newly_registered"] # None — .de publishes no RDAP, so age is unknown
None means we could not determine it. It is not a negative finding.
Treating it as "established" is exactly the mistake the field exists to
prevent.
Same for mailbox_exists: Gmail and Yahoo accept mail for addresses that do
not exist, so we return None rather than a guess.
Errors
from layercall import LayerCallError
try:
lc.score_ip(ip)
except LayerCallError as err:
if err.is_quota: # 402 — out of quota or spend cap reached
...
if err.is_auth: # 401/403 — bad or revoked key
...
print(err.request_id) # quote this in a support ticket
except Exception:
pass # fail open: let the signup through
429s and 5xx retry automatically (2 attempts, short backoff). Other 4xx fail fast — they will not succeed on a retry.
Server-side only
An API key in anything a user can read is public and bills to your account.
License
MIT
Release files for layercall 1.2.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| layercall-1.2.9.tar.gz | 12.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| layercall-1.2.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:23.9 kB
Release files / layercall-1.2.9.tar.gz
| Download URL | layercall-1.2.9.tar.gz |
|---|---|
| Size | 12.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
58622528c0b4bb5ceed8b6922da2b334493aa3fbc29f24319504a958871ecfe4
|
|
BLAKE2b-256 checksum How to use checksums |
b2bc40522c663d7e8bf02eb79a781224a3be09f0fece515f21b085e50243797e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency logRelease files / layercall-1.2.9-py3-none-any.whl
| Download URL | layercall-1.2.9-py3-none-any.whl |
|---|---|
| Size | 10.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
849256d1483b35529b5381969a205ee90a36f8bd21adfac50d0e819d37d6e953
|
|
BLAKE2b-256 checksum How to use checksums |
364fd955c526fce7cf632aef11d4a5b35ffd45dfd42f03046a0893b46a5f779a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency log