SDKs
Try it with no install
export LAYERCALL_API_KEY=tl_live_... npx layercall ip 8.8.8.8 npx layercall email someone@mailinator.com npx layercall user --ip 1.2.3.4 --email a@b.com
Node / TypeScript
npm i layercall
import { LayerCall } from "layercall"; const lc = new LayerCall(process.env.LAYERCALL_API_KEY!); const result = await lc.scoreUser({ ip: req.ip, email: body.email, phone: body.phone, }); if (result.verdict === "review") { await sendOtp(body.email); // a real user clears it themselves }
Python
pip install layercall
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"])
Two things the clients enforce
Server-side only. The Node client throws if it detects a browser. An API key shipped to a browser is public the moment someone opens devtools, and it bills to your account.
Retries only what can succeed. 429 and 5xx retry with short backoff; other 4xx fail fast, because they will not succeed on a retry and this code sits in front of a user.
null means unknown, never “no”
newly_registered is null when a TLD publishes no RDAP — .de, .ru and .ac.uk among them — and mailbox_exists is null when the provider does not answer honestly. Gmail and Yahoo accept mail for addresses that do not exist, so any vendor claiming certainty there is inventing it. Treating either as a negative finding is the exact mistake those fields exist to prevent.
Prefer a challenge to a rejection on review — see acting on a verdict.
Drop-in middleware
The Node package ships Express and Next.js middleware as separate entry points. Neither needs any code beyond the import: the visitor’s IP is scored on the way in and the result is attached to the request.
import layercall from "layercall/express"; app.use(layercall({ apiKey: process.env.LAYERCALL_API_KEY })); app.post("/signup", (req, res) => { const t = req.trust; // { scored, verdict, risk_score, result } if (t.scored && t.verdict === "block") return res.status(403).end(); // ... });
import { withTrust } from "layercall/next"; export const POST = withTrust(async (req, trust) => { if (trust.scored && trust.verdict === "block") { return Response.json({ error: "blocked" }, { status: 403 }); } return Response.json({ ok: true }); });
Always check scored before acting on verdict. When we are unreachable the middleware fails open — it lets the request through with scored: false rather than taking your site down with ours. That is almost always what you want, but it means an allow you did not check scored against might be an outage rather than a judgement. For a payment or a password reset, treat scored: false as a reason to challenge.