Teams often treat an IP risk score as a fraud probability: 88 means "an 88% chance this user is an attacker," and 0 means "safe." That reading is the most common way an IP feed gets misused. On IP99 by ThreatHunter, risk.score measures the freshness and strength of verifiable evidence that an address is currently in risk infrastructure (proxy, VPN, dial-up pool, cloud phone, and similar signals), and the number decays as that evidence ages. It is not the probability that the person behind the address intends fraud.

This walkthrough stays on the free, no-key profile GET /v1/ip/{ip}. JSON samples were verified on 14 November 2026 UTC. Notes: IP99, API, IP risk score, evidence freshness.

What the free lookup returns

  • risk.score — 0–100, freshness and strength of verifiable risk evidence, decaying with its age.
  • risk.level — none (0), low (1–29), medium (30–69), high (70–100).
  • evidence_state — active (fresh evidence that still scores), stale (seen before, evidence expired, score 0, still a weak prior), none (never seen).
  • risk.signals — matched slugs only, alphabetical; the key is absent when nothing matched. hosting and mobile are network attributes, not risk verdicts — official guidance is not to block on those slugs alone.
  • computed_at — when this verdict was computed.

Official docs are explicit: two addresses with the same score can carry very different fraud risk depending on your own signals. The score tells you what the network has been seen doing, not what the user intends. Branch on evidence_state before the number.

The same 0, two different meanings

Start with a well-known resolver:

curl https://ip99.com/v1/ip/8.8.8.8 

Verified 14 November 2026 UTC (computed_at 2026-11-14T13:06:08Z):

{ "ip": "8.8.8.8", "computed_at": "2026-11-14T13:06:08Z", "evidence_state": "none", "geo": { "continent": "NA", "country": "US", "region": "California", "city": "Mountain View", "tz": "America/Los_Angeles" }, "network": { "asn": 15169, "usage_type": "IDC" }, "risk": { "score": 0, "level": "none", "signals": ["hosting"] } } 

Score 0 and evidence_state: "none" mean IP99 holds no current risk evidence for this address. hosting is here because the usage type is IDC, not because Google Public DNS is a proxy. Marking 8.8.8.8 "safe" because the score is 0, or blocking it because hosting is present, both misread the fields.

Now look up an address that used to score high:

curl https://ip99.com/v1/ip/78.141.167.222 

Same day (computed_at 2026-11-14T13:06:17Z):

{ "ip": "78.141.167.222", "computed_at": "2026-11-14T13:06:17Z", "evidence_state": "stale", "geo": { "continent": "EU", "country": "LU", "region": "Luxembourg", "city": "Luxembourg", "tz": "Europe/Luxembourg" }, "network": { "asn": 6661, "usage_type": "DYN" }, "risk": { "score": 0, "level": "none", "signals": ["proxy"] } } 

The score is also 0. The meaning is not. evidence_state: "stale" plus signals: ["proxy"] means this address was observed as a proxy exit, and that evidence has aged past the decay window. On 9 October 2026 UTC, a lookup of the same address returned evidence_state: "active", score 97, level high, and proxy on residential (DYN) space. Today the number is 0 because the judgement expired — not because the product "cleared" the address.

That is the official distinction on the IP risk score guide: 0 + none is "unknown"; 0 + stale is "was, and probably no longer is." Neither is a clean bill of health. If your rule is if score == 0: allow, both of these addresses take the same path, and you have thrown away the only field that distinguishes them.

The score can decay while it is still active

A documented sample on IP99's evidence freshness page was captured on 13 September 2026, when 122.99.101.50 was being sold as a residential exit: evidence_state: "active", score 78, level high, signals: ["proxy"], usage_type: "DYN", ASN 17471, computed_at 2026-09-13T15:41:18Z.

The same address today (computed_at 2026-11-14T13:06:22Z) is still active and still proxy on DYN space in Dhaka, ASN 17471, but the score is 16 and the level is low. The snapshot also carried degraded: true, meaning some data sources were unavailable when the verdict was computed; the fields above are still the live free-profile answer.

curl https://ip99.com/v1/ip/122.99.101.50 

That is the score working as designed: still active, but weaker than in September. Caching "this IP is high-risk" from September, or even from yesterday, turns a true statement into a false one on a timer you cannot see.

A high score is not a ban reason by itself

curl https://ip99.com/v1/ip/178.20.30.146 

Verified the same day (computed_at 2026-11-14T13:06:20Z; this snapshot also had degraded: true):

{ "ip": "178.20.30.146", "computed_at": "2026-11-14T13:06:20Z", "evidence_state": "active", "geo": { "continent": "EU", "country": "FR", "region": "Île-de-France", "city": "Paris", "tz": "Europe/Paris" }, "network": { "asn": 46475, "usage_type": "IDC" }, "risk": { "score": 88, "level": "high", "signals": ["hosting", "osint", "proxy"] } } 

88 is not "an 88% chance this session is fraud." It means the currently verifiable evidence that this datacenter address is a proxy exit (and is listed in OSINT sources) is strong and still fresh. Official guidance is to step up — a second factor, a delay, or manual review — not to hard-ban, especially where carrier-grade NAT and residential pools share exits with real customers. osint is a list hit, not a probability that the operator is an attacker; hosting is the usage-type attribute again.

How to branch on it

The official shape:

if evidence_state == "active" and risk.level in ("high", "medium"): step_up() # second factor, delay, manual review — not a ban elif evidence_state == "stale": weak_prior() # combine with device, velocity, account age else: # "none" no_information() # NOT "safe": fall back to your other signals 

Two habits keep this honest: query at decision time (reuse one response only for the seconds of a single decision), and store computed_at plus evidence_state with the decision so a disputed block can be traced to when it was observed. Free /v1/ip answers for now only and rejects ?at= with 400 at_unsupported. Historical replay is a Pulse feature (?at= on /pulse/v1/ip/{ip}, up to 30 days). Without a professional key, Pulse returns 403 professional_required.

Generic triage (not a contract): score 0 → do not stamp "clean"; 1–29 → observe; 30–69 → captcha / throttle / second factor; 70–100 → block only when business cost allows, and not at building or prefix scale. Official order: evidence_state → score/level → signal type, then usage_type on the free profile (share_tier on Pulse). If hijacked_proxy is present, challenge the session; do not ban the account — the address holder is the victim.

Anonymous quota is 500 lookups/day per client IP (60/min); a free logged-in key raises the daily cap to 5,000/day. Check remaining with GET https://ip99.com/v1/me or the X-Daily-* headers. OpenAPI: https://ip99.com/openapi.json. Pricing: https://ip99.com/en/pricing.

Do not treat IP99 scores as identical to other ThreatHunter datasets — this article only claims what free /v1/ip and the cited product pages document.

Try it

curl https://ip99.com/v1/ip/8.8.8.8 curl https://ip99.com/v1/ip/78.141.167.222 curl https://ip99.com/v1/ip/122.99.101.50 curl https://ip99.com/v1/ip/178.20.30.146 curl https://ip99.com/v1/me 

IP99 is a ThreatHunter product. Read evidence_state before you treat a 0 as "safe" or an 88 as a probability. Start at https://ip99.com; the field contract is on What an IP risk score means, evidence freshness, and https://ip99.com/api.