n8n
No community node exists yet. The built-in HTTP Request node does everything required, and on self-hosted n8n there is no per-task cost.
Setup
- 1Add an HTTP Request node, method POST.
- 2URL: https://www.layercall.com/v1/score/user
- 3Authentication: Generic Credential Type → Header Auth, name X-Api-Key, value your key.
- 4Body Content Type: JSON, then map ip, email and phone from the previous node.
- 5Follow it with an IF node branching on {{ $json.verdict }}.
Code
// n8n → Code node, mode "Run Once for Each Item". // // Preferred over HTTP Request + IF when you want the call, the fallback and // the merge back onto the item in one node. const key = $env.LAYERCALL_KEY; let scored; try { scored = await this.helpers.httpRequest({ method: "POST", url: "https://www.layercall.com/v1/score/user", headers: { "X-Api-Key": key, "Content-Type": "application/json" }, body: { ip: $json.ip, email: $json.email, phone: $json.phone, }, json: true, timeout: 5000, }); } catch (e) { // Fail open, and mark it so a later node can sweep these up. scored = { verdict: "allow", risk_score: 0, degraded: true }; } return { json: { ...$json, verdict: scored.verdict, risk_score: scored.risk_score, summary: scored.summary, degraded: scored.degraded ?? false, }, };
What to do with each verdict
Three outcomes, and the middle one is the one worth getting right — refusing a real customer usually costs more than reviewing them.
| Verdict | In n8n |
|---|---|
| allow | Fall through the IF node's true branch and carry on. |
| review | Route to a Slack node posting {{ $json.summary }}, then a NoOp so the item still reaches the merge. |
| block | Route to a Stop And Error node, or write to a rejects table. An IF with three outputs is a Switch node — use that rather than nesting two IFs. |
What comes back
A real response, generated from the live API rather than written by hand. Branch on verdict; summary is a sentence written to be shown to a person, and components_checked tells you what actually went into the score.
Show the full response(POST /v1/score/user)
{ "risk_score": 65, "verdict": "review", "summary": "Needs review (65/100) — Tor exit node, commercial VPN and datacenter ASN.", "components": { "email": { "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 } }, "phone": { "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 }, "ip": { "ip": "185.220.101.1", "risk_score": 60, "signals": { "is_vpn": true, "is_proxy": true, "is_datacenter": true, "is_tor": true, "recent_abuse": false, "is_hijacked_netblock": false }, "geo": { "country": "DE", "city": "Berlin", "asn": "AS60729", "isp": "Stiftung Erneuerbare Freiheit" }, "vpn_provider": null, "hijacked_source": null, "verdict": "review", "abuse_reports": 0 } }, "components_checked": [ "email", "phone", "ip" ], "linkage": { "device_email_count": null, "email_device_count": 0, "email_ip_count": 0, "subnet_rate_1h": 2, "domain_rate_1h": 0 }, "actor": { "type": "unknown", "proven": false, "basis": "none", "operator": null, "trigger": null, "detail": "No signature and no device fingerprint. Drop fp.js on the page, or pass the agent's signed request, to get an answer here." }, "top_signals": [ "ip: tor exit node", "ip: commercial vpn", "ip: datacenter asn", "email: disposable domain", "email: role account", "email: machine-generated handle" ] }
Traps specific to n8n
Self-hosted n8n ships with N8N_BLOCK_ENV_ACCESS_IN_NODE=true, so $env.LAYERCALL_KEY returns undefined and the call 401s with no obvious cause. Either set that variable to false, or put the key in a Header Auth credential and use the HTTP Request node instead — the credential store is the better answer on a shared instance.
It is available in the Code node. In a Function node on older versions it is not, and in expressions it never is. If you get 'Cannot read properties of undefined', check the node type before the code.
500 items through a per-item HTTP Request node is 500 round trips. POST to /v1/batch with up to 500 values of one type instead — one request, one connection.
Questions
Can I batch a whole list in n8n?
Yes — POST to /v1/batch with up to 500 values of one type per request, which is far faster than looping a single-record node 500 times and much kinder to both sides.
Does this work on n8n Cloud as well as self-hosted?
Yes. The HTTP Request node and the Code node both exist on Cloud. The only difference is $env, which Cloud does not expose at all — on Cloud, use a Header Auth credential.