The reason people stay on a fraud API they have outgrown is almost never the argument. It is that moving means touching parsing code, alert thresholds, dashboards and tests — a week of engineering against a saving that does not cover it. So LayerCall speaks the shape you already parse.
The change
Before
https://ipqualityscore.com/api/json/ip/YOUR_KEY/1.2.3.4
After
https://www.layercall.com/compat/ipqs/ip/YOUR_LAYERCALL_KEY/1.2.3.4
That is the whole migration for the IP endpoint, and /email/ and /phone/ work the same way. If you would rather not put a key in a URL — and you are right not to want that — send it as X-Api-Key instead and leave anything in the key position; the header wins. We never record the path you called, only the value you looked up.
What maps to what
IP — /compat/ipqs/ip/KEY/1.2.3.4
| Their field | What we put in it |
|---|---|
| fraud_score | risk_score — both 0–100, both riskier as they rise |
| country_code / city | geo.country / geo.city |
| ISP / organization | geo.isp — we hold one name for the network |
| ASN | geo.asn, as a number (AS15169 → 15169) |
| proxy | proxy OR VPN OR Tor, matching their wider meaning |
| vpn / tor | signals.is_vpn / signals.is_tor |
| recent_abuse | signals.recent_abuse |
| connection_type | "Data Center" when the network is one; null otherwise |
Email — /compat/ipqs/email/KEY/you@example.com
| Their field | What we put in it |
|---|---|
| valid | syntax and MX both good, and the mailbox not known to be absent |
| disposable | signals.is_disposable |
| fraud_score | risk_score |
| deliverability | our 0–100 deliverability score, bucketed high / medium / low |
| catch_all / dns_valid | signals.is_catch_all / signals.mx_found |
| generic / common | role address / large consumer provider |
| leaked | seen in a known breach |
| sanitized_email / suggested_domain | normalized_email / did_you_mean |
| domain_age | domain age in days, in their object shape |
Phone — /compat/ipqs/phone/KEY/+14155552671
| Their field | What we put in it |
|---|---|
| valid | signals.syntax_valid |
| formatted / local_format | international / national notation |
| fraud_score | risk_score |
| VOIP | signals.is_voip — their capitalisation, kept |
| risky | anything our verdict would not wave through |
| line_type / country | number.line_type / number.country |
| dialing_code | derived from the parsed number |
Every response also carries a layercall object with the signals that have no counterpart on their side — the verdict, hijacked-netblock detection, our own abuse-report counts, whether the answer was cached. Ignore it and nothing changes; read it when you are ready to use what you moved for.
What we return null for, and why
A compatibility layer that fills every field so the response looks complete is a compatibility layer that lies. Anything we do not actually measure comes back null — not false, which would assert we checked and found nothing. This is the complete list.
ip
active_vpnactive_torbot_statusis_crawlermobilelatitudelongitudezip_coderegiontimezonehostabuse_velocityLive-probe and per-client fields (is this exit node up right now, is this a bot, where exactly is it) need measurements we do not take on an address alone. Bot scoring lives on /v1/score/device, which takes signals an IP cannot carry.
first_namehoneypotspam_trap_scorefrequent_complainersmtp_scoreoverall_scoreTheir smtp_score and overall_score are scales of their own with no published definition, so there is nothing to convert honestly. The rest are signals we do not collect.
phone
activeactive_statusprepaidnamecarriercityregionzip_codesms_domaindo_not_callleakedspammerWhether a handset is switched on, who it belongs to and which carrier holds it are HLR and CNAM queries — a different and far more expensive product. We would rather say so than guess.
Two things that will differ
The scores themselves
fraud_score is 0–100 on both sides and means the same direction, so your threshold keeps compiling. It will not keep meaning the same thing — different engines disagree, which is the entire reason anyone compares them. Run a real list through both before you move a threshold you rely on. The bulk checker does that with no account.
HTTP status on failure
A rejected key gets a real 4xx here, not a 200 with success: false. The body still carries success, so code branching on it is unaffected — but a proxy, a retry policy or an uptime check between you and us will now see a refusal as a refusal.
The free tier is 1,000 lookups a month with no card and no daily cap, which is enough to point a copy of your real traffic at both and compare the answers rather than the marketing.
IPQualityScore is a trademark of its owner. LayerCall is not affiliated with or endorsed by them. This page documents a response format we implement so their customers can leave without rewriting; every field above is produced by LayerCall’s own engine.