An IP risk score is a 0 to 100 rating of how much abuse an IP address has been associated with. Every guide tells you to respond proportionally to it. Almost none of them name a number.
The page currently ranking first for "check IP risk score before processing payment" has a section titled "Define Action Tiers." It does not define a tier. The next two results are a vendor glossary and a three-year-old payment processor blog post. Between them: 5,000 words, zero thresholds, zero code.
So this is the reference I wanted and could not find. The bands, the fields underneath them, the action each one should trigger, and the parts where the score is no help at all.
TL;DR
- Four bands: 1-19 allow with standard controls, 20-44 combine with other signals before acting, 45-79 add friction, 80-100 block or route to manual review.
- A clean IP returns
0. Scores above 20 indicate additional risk context, but the category-specific flags explain what produced it. - Three fields should override the band:
is_residential_proxy,is_known_attacker, and the confidence scores. A residential proxy scoring 30 is not the same animal as a cloud IP scoring 30. -
is_relayshould almost never block on its own. That flag includes privacy services such as iCloud Private Relay and Cloudflare WARP, so it can represent legitimate consumer traffic. - The gate belongs before the payment call, because plenty of abuse never reaches a payment object at all.
- Stripe Radar is not a substitute and not a competitor. It scores a different thing at a different moment.
The score is a decision aid, not a verdict. It answers "how much abuse has this address been involved in," which is a narrower question than "is this person committing fraud." Treat the bands below as a starting policy and tune them against your own chargeback and false-positive numbers.
What an IP risk score actually measures
Vendors cannot agree on a name for this number. You will see it sold as an IP risk score, an IP fraud score, a fraud risk score, or an IP reputation score. Across the fraud detection APIs that expose one, it is the same idea under different labels, which is worth knowing before you compare two vendors' documentation and think you are reading about two different products.
A risk score is a composite of three things: whether the address is hiding its origin, whether it has abuse history, and what kind of infrastructure it sits on. Nothing in there is about the person holding the card.
That distinction matters more than it sounds. A score of 60 does not mean a 60% chance of fraud. It means several signals fired at once on an address that a lot of traffic shares. Corporate VPNs score. Datacenter egress scores. A university NAT gateway scores. All three are full of legitimate customers.
The composite is built from active honeypots, blocklists, VPN and proxy enumeration, and behavioural analysis, which is roughly the industry pattern. Field names below are ipgeolocation.io's, because that is the API I use for this, but the concepts port: IPQualityScore, MaxMind minFraud, IPLocate, IP2Proxy, and Spur all expose some version of a composite score plus category flags. What varies is whether you get the provider name and the confidence behind each flag, and that turns out to be the part that changes decisions.
The four bands and what to do in each
This is the table. Everything else in the article supports it.
threat_score |
Reading | Action | What it usually means |
|---|---|---|---|
| 1-19 | Low observed risk | Allow with standard controls | Isolated, low-severity indicators. Often just cloud hosting. Still worth logging for pattern analysis. |
| 20-44 | Use with context | Combine with other signals before acting | Anonymization without attacker or abuse signals, or several light indicators adding up. Blocking here catches real users. |
| 45-79 | Elevated risk | Add friction before allowing | Anonymization plus abuse indicators such as bot activity or spam history. OTP, CAPTCHA, email verification, rate limiting, or a review queue. |
| 80-100 | High risk | Block, hard challenge, or manual review | Multiple signals at once: VPN or proxy alongside attacker history, spam reports, or bot-like behaviour. |
Two notes on reading it. A clean address returns 0, and the documented bands start at 1, so treat 0 and the 1-19 band identically. And the 20-44 row is the one people get wrong: it is tempting to block there because "anonymization" sounds damning, but an IP showing a VPN and nothing else is a privacy-conscious customer far more often than a fraudster. The published threshold guidance is explicit that blocking at that level without device, behaviour, velocity, or geolocation context risks catching legitimate users.
If you implement only the 80-100 rule and log everything else for a month, you will have done most of the useful work and broken nothing.
The 19 fields and what each one means for a payment decision
The threat score is the summary. These are the inputs, and several of them should change your decision independently of the number.
| Field | Type | What it tells you at checkout |
|---|---|---|
threat_score |
number, 0-100 | The composite. Start here, do not stop here. |
is_anonymous |
boolean | True when a VPN, proxy, Tor, or relay is hiding the origin. The umbrella flag over the four below. |
is_vpn |
boolean | Commercial VPN. Extremely common among legitimate customers. |
vpn_provider_names |
array of strings | Named provider, for example ["Nord VPN"]. Empty array when unattributed. |
vpn_confidence_score |
number, 0-100 | How strongly the VPN detection is supported. |
vpn_last_seen |
string, YYYY-MM-DD
|
Recency of the detection. Empty string when absent. |
is_proxy |
boolean | Datacenter or commercial proxy. |
proxy_provider_names |
array of strings | For example ["Zyte Proxy"]. Scraping infrastructure surfaces here. |
proxy_confidence_score |
number, 0-100 | Detection strength for the proxy signal. |
proxy_last_seen |
string, YYYY-MM-DD
|
Recency. Empty string when absent. |
is_residential_proxy |
boolean | Residential proxy network. The flag most APIs do not have, and the one that matters most here. |
is_tor |
boolean | Tor exit node. |
is_relay |
boolean | iCloud Private Relay or Cloudflare WARP. Not evasion. See below. |
relay_provider_name |
string | Empty string when is_relay is false. |
is_known_attacker |
boolean | Observed participating in attacks. Escalate regardless of score. |
is_bot |
boolean | Automated client. |
is_spam |
boolean | Spam-source history. |
is_cloud_provider |
boolean | Hosting infrastructure. Expected for server-to-server, odd for a consumer checkout. |
cloud_provider_name |
string | For example "Packethub S.A.". |
Note the empty-value conventions, because they will bite you: unattributed provider lists come back as [], confidence scores as 0, and date fields as "" rather than null. A truthy check on proxy_last_seen behaves the way you want; a !== null check does not.
The three fields that should override the band
is_known_attacker is the strongest single signal in the response. An address observed in actual attacks is not a scoring question. Block it at 30 the same way you would at 90.
is_residential_proxy is the one worth paying for. Residential proxy networks route traffic through real consumer devices, which is exactly what defeats a naive datacenter-IP rule. A residential proxy at a checkout is someone who went to deliberate trouble to look like a normal customer. I route those to review rather than blocking, because the false-positive cost is real: some are malware-compromised devices belonging to actual people, and a few are legitimate privacy tools.
The confidence scores are the field most teams ignore and should not. is_vpn: true with vpn_confidence_score: 30 and a vpn_last_seen from four months ago is a much weaker claim than the same flag at 90 seen last week. If your rule engine treats those identically, you are throwing away the only part of the response that tells you how much to trust it.
The one flag that should almost never block
is_relay covers iCloud Private Relay and Cloudflare WARP. Private Relay is included with iCloud+, but users must enable it on each device. Relay status alone is not evidence of fraud and should not trigger a block without additional risk signals.
Treating relay traffic as VPN traffic is the most expensive mistake in this whole area, and it is common because a lot of APIs collapse both into one boolean. If your provider does not separate is_relay from is_vpn, you cannot write this rule at all, and you are quietly adding friction to iPhone checkouts. Allow relay below the elevated band and let the score do the work above it.
Where the gate goes
Before the payment call, after you have resolved the real client IP. This is a pre-authorization check in the literal sense: it runs while you still have cheap options, before there is an authorization to reverse. One request:
curl -sS -X GET \
"https://api.ipgeolocation.io/v3/security?apiKey=$IPGEO_API_KEY&ip=2.56.188.34"
The full response, all 19 fields, for a heavily flagged address:
{
"ip": "2.56.188.34",
"security": {
"threat_score": 80,
"is_tor": false,
"is_proxy": true,
"proxy_provider_names": ["Zyte Proxy"],
"proxy_confidence_score": 80,
"proxy_last_seen": "2025-12-12",
"is_residential_proxy": true,
"is_vpn": true,
"vpn_provider_names": ["Nord VPN"],
"vpn_confidence_score": 80,
"vpn_last_seen": "2026-01-19",
"is_relay": false,
"relay_provider_name": "",
"is_anonymous": true,
"is_known_attacker": true,
"is_bot": false,
"is_spam": false,
"is_cloud_provider": true,
"cloud_provider_name": "Packethub S.A."
}
}
That address is a Zyte proxy, a Nord VPN exit, a residential proxy, a known attacker, and cloud-hosted, all at once. The individual flags explain why the address falls in the high-risk band and provide more actionable context than the score alone, and once you are in the top band the individual flags tell you more than the number does.
The Node side, on Node 18 or later where fetch and AbortSignal.timeout are built in:
const RISK_ENDPOINT = "https://api.ipgeolocation.io/v3/security";
// Fail open. A checkout that 500s because a risk lookup timed out costs
// more than the fraud that lookup would have caught.
const UNAVAILABLE = { action: "allow", band: "unknown", reason: "lookup-unavailable" };
async function fetchRisk(ip) {
const apiKey = process.env.IPGEO_API_KEY;
if (!apiKey) throw new Error("IPGEO_API_KEY is not set");
const url = `${RISK_ENDPOINT}?apiKey=${apiKey}&ip=${encodeURIComponent(ip)}`;
// 1500ms is generous for this endpoint and still inside the budget of a
// request a human is watching a spinner for.
const res = await fetch(url, { signal: AbortSignal.timeout(1500) });
if (!res.ok) throw new Error(`risk lookup failed: HTTP ${res.status}`);
return res.json();
}
function decide(security) {
// Optional chaining throughout: any field can be missing on a partial
// response, and a thrown TypeError here would block a paying customer.
const score = security?.threat_score ?? 0;
// Overrides first. These two outrank the band in both directions.
if (security?.is_known_attacker === true) {
return { action: "block", band: "override", reason: "known-attacker" };
}
if (security?.is_residential_proxy === true) {
return { action: "review", band: "override", reason: "residential-proxy" };
}
// Private Relay and WARP are privacy routing, not evasion. Blocking these
// can add unnecessary friction for legitimate privacy-relay users.
if (security?.is_relay === true && score < 45) {
return { action: "allow", band: "relay", reason: "privacy-relay" };
}
if (score >= 80) return { action: "block", band: "80-100", reason: "high-risk" };
if (score >= 45) return { action: "step_up", band: "45-79", reason: "elevated-risk" };
if (score >= 20) return { action: "context", band: "20-44", reason: "needs-context" };
return { action: "allow", band: "1-19", reason: "low-risk" };
}
async function gate(req) {
// Configure Express `trust proxy` for the specific reverse proxies or subnets in front of this application, then read req.ip.
// Do not trust X-Forwarded-For directly or enable trust proxy globally unless every path to the app passes through a trusted proxy.
// Example only: replace these with the addresses or subnets of the
// reverse proxies that are allowed to supply forwarding headers.
// app.set("trust proxy", ["loopback", "10.0.0.0/8"]);
const ip = req.ip;
try {
const payload = await fetchRisk(ip);
return decide(payload?.security ?? {});
} catch (err) {
console.error(`[risk-gate] ${ip}: ${err.message}`);
return UNAVAILABLE;
}
}
module.exports = { gate, decide };
Two things in there are load-bearing. The trust proxy note, because reading X-Forwarded-For directly is the single most common way this gets built wrong: the header is client-controlled, so an attacker just sets it to 8.8.8.8 and walks through your gate. And the fail-open default, which is a deliberate business call rather than a technical one. If your risk provider goes down, you either lose revenue or accept a few hours of unscreened traffic. I take the unscreened traffic. If you would rather fail closed, that is defensible, but decide it on purpose instead of discovering your choice during an outage.
The action strings are deliberately not booleans. context in particular is not a decision, it is an instruction to go look at your other signals, and collapsing it into allow or block throws away the entire point of the middle band.
Why not just use Stripe Radar
Fair question, and Radar ranks on the search results for this topic, so plenty of people are asking it.
Radar wins on signal. Its adaptive model uses hundreds of risk factors per payment plus data across Stripe's whole network, which is a dataset no IP API has. Its rules take four actions: request 3DS, allow, block, or review. If you are only going to run one of these two things, run Radar.
Three gaps are worth knowing about. Radar's granular 0 to 99 risk score, with its default 65 elevated and 75 high-risk thresholds, is a Radar for Fraud Teams feature rather than something you get with base Radar. Radar scores the initial payment of a recurring subscription and then evaluates rules without rescoring subsequent ones. And Radar operates on Charges, PaymentIntents, and SetupIntents, meaning it needs a payment object to exist before it has an opinion.
That last one is the actual argument for an IP gate, and it is not really a fraud argument. Card testing sweeps, credential stuffing against your login, coupon enumeration, and free-trial farming mostly never create a payment object at all. Radar cannot see any of it. A risk check at the edge covers requests that never become charges, which is a different job in a different place.
The other gap is explainability. Radar returns a score and an outcome. It does not tell you the connection came through a Zyte proxy last seen twelve days ago with 80% confidence. When you are writing a rule, or arguing with a customer whose order you held, that detail is the difference between a policy and a shrug.
Run both. Use the IP score to decide who gets to attempt a payment, and let Radar score the payment itself.
Tuning the bands against your own numbers
The four bands are a vendor default, and the vendor says so in the same breath as publishing them. Nobody else's thresholds are calibrated to your traffic mix.
Measure two things per band before you touch anything: what share of traffic lands in it, and what your chargeback rate is inside it. If your 80-100 band is 0.2% of traffic and carries a third of your chargebacks, the rule is working. If it is 6% of traffic and carries no more chargebacks than the 1-19 band, you are blocking customers for nothing, and the fix is to raise the threshold rather than to add more signals.
Here is the arithmetic people skip. Suppose 8% of your checkouts land in 45-79 and you send them all to a step-up, and 70% of those complete it. You have just added friction to 8% of checkouts and lost 2.4% of them outright. Whether that trade is good depends entirely on what fraud those 8% were actually carrying, and you cannot know until you have run the band in log-only mode for a few weeks.
The trap is tightening after a bad week. One chargeback cluster feels like evidence and is usually noise. Change one threshold at a time, wait out at least one full dispute cycle before judging it, and keep the log-only comparison running so you can tell what the old policy would have done.
What this will not catch
Worth being blunt about the ceiling, because a reference that oversells is worse than no reference.
Card testing from clean residential addresses defeats it completely. So does anything where the customer is genuinely who they claim to be and disputes the charge afterward, which is a large share of all chargebacks and has nothing to do with the network path. Account takeover from the victim's own home connection looks perfect on every field in that table. And an attacker who wants a clean residential IP can rent one for the price of a coffee, which is the honest limit of every IP-based control.
IP risk scoring is a cheap filter that removes the lazy and the automated. It signals, it does not prove, and any rule that treats a flag as a verdict on a customer will eventually hold an order it should not have.
A few extra notes
The security fields require a paid plan, starting at $19 a month with 150,000 credits. A security lookup costs 2 credits, so that allowance is 75,000 checks rather than 150,000, which is the kind of arithmetic worth doing before you put the call on every request. The free tier returns core geolocation and no security object at all. Credits are only charged on HTTP 200, so timeouts and errors cost nothing, and the X-Credits-Charged response header reports the actual charge per request.
Cache by IP with a short TTL, five to fifteen minutes depending on traffic. Risk data moves, last_seen dates shift, and an address that was clean this morning can be enumerated into a proxy pool by tonight. Caching for a day is how you end up allowing something you already know about.
If you want the country-mismatch layer that sits alongside this one, comparing IP country to billing country is a separate and complementary check, and I wrote that up separately.
Both IPv4 and IPv6 are supported on the same endpoint, which matters more than it used to now that a real share of mobile traffic arrives over v6.
Start with the 80-100 rule and log the rest. Give it a month, look at where your chargebacks actually landed, and then decide whether the middle bands earn the friction they cost.
Top comments (0)