A chargeback lands weeks after checkout. Your fraud queue opens the order, copies the billing IP, and hits today's IP reputation API. The score is 0. Someone stamps the dispute "clean egress" and moves on.
That workflow asks the wrong question. IP risk on IP99 by ThreatHunter is a judgement that expires: the score measures freshness and strength of verifiable evidence, not "probability this cardholder is an attacker." An address that looked like a dial-up proxy exit at purchase may show evidence_state: none and score 0 by the time Finance asks. Re-querying now does not reconstruct then.
Live gate vs chargeback clock
| Moment | Question | Free GET /v1/ip/{ip}
|
Pulse GET /pulse/v1/ip/{ip}?at=
|
|---|---|---|---|
| Checkout / claim | Is this egress risky right now? | Yes — no key required | Same score, more evidence detail |
| Chargeback / audit | What did we know at order time? | No — at returns 400 at_unsupported
|
Yes — professional tier + history enabled, ~30 days back |
Official product split: basic endpoint for real-time control (sign-up, sign-in, pre-order); Pulse for after-the-fact review, reconciliation, and strategy back-testing. One IP, one score — Pulse is not a "better verdict," only a deeper and time-travel view of the same judgement.
What to store at decision time (so replay is possible)
If you only keep the final allow/deny bit, chargeback week has nothing to replay. At the gate that matters (checkout, claim, payout), persist at least:
- Client egress IP (server-observed — never trust a body field the browser sent).
-
computed_atfrom the IP99 response (RFC 3339). -
evidence_state,risk.score/risk.level, andrisk.signals(or "signals absent"). -
network.asnandnetwork.usage_typefor blast-radius context when you lack Pulseshare_tier.
That snapshot is your audit trail on the free tier. Historical ?at= later lets you re-ask the feed as of that moment; it does not replace logging what your gate actually used.
How to read a replay (same triage order)
Whether you look at the stored snapshot or a Pulse history response, official reading order still applies:
-
evidence_state— can this conclusion be trusted at that moment?nonemeans never seen (coverage gap), not "safe."stalemeans seen before but evidence aged out (score 0, weak feature only). -
risk.score/risk.level— only meaningful once state isactiveorstale. -
risk.signals— why it scored:proxy,vpn,dialup_pool,cloud_phone,hijacked_proxy, … matter differently for dispute letters than barehosting(a network property — Google Public DNS carrieshostingwith score 0). -
Share scope — Pulse
share_tier(T0–T3) if you ever blocked; on free responses useusage_type(DYN/MOBvsIDC) as a rough prior. Do not equate one residential NAT ban with one exclusive VPS ban. -
Degraded marker — basic body
degraded: true, or Pulse headerX-Pulse-Degraded: 1— discount the answer.
hijacked_proxy still means the line owner is likely a victim; a chargeback write-up that says "banned the household forever" is usually the wrong story even when proxy evidence was active at purchase.
Live curls (2026-10-27, no API key)
Anonymous quota: 500 lookups/day per client IP, 60/min. Sign in for 5,000/day. Overseas self-serve Pro / Pro+ buys volume only ($29 / $99) — same evidence fields on /v1/ip, not a better score. Per-item evidence, share_tier, and ?at= history are a separate professional / contracted Pulse capability.
# What the free API answers: now only
curl https://ip99.com/v1/ip/8.8.8.8
# Docs demo residential address — mask the last octet in tickets
curl https://ip99.com/v1/ip/27.22.66.200
# Remaining anon quota (does not spend a lookup on some setups; still cheap to check)
curl https://ip99.com/v1/me
# History needs Pulse + professional key — free /v1/ip rejects at=
# curl -H "X-API-Key: YOUR_KEY" \
# "https://ip99.com/pulse/v1/ip/86.111.136.23?at=2026-09-14T09:29:23Z"
Sample A: datacenter control (score 0 is not a chargeback win)
Captured ~2026-10-27 13:13 UTC:
{
"ip": "8.8.8.8",
"computed_at": "2026-10-27T13:13:13Z",
"evidence_state": "none",
"geo": {
"country": "US",
"region": "California",
"city": "Mountain View",
"tz": "America/Los_Angeles"
},
"network": { "asn": 15169, "usage_type": "IDC" },
"risk": {
"score": 0,
"level": "none",
"signals": ["hosting"]
}
}
hosting here is network shape, not proxy proof. evidence_state: none with score 0 means IP99 holds no current verifiable risk evidence — not that the payment was legitimate.
Sample B: residential dial-up / proxy supply (mask last octet)
Same day live lookup. Full address works in the curl above; tickets should show 27.22.66.x.
{
"ip": "27.22.66.x",
"computed_at": "2026-10-27T13:13:15Z",
"evidence_state": "active",
"geo": {
"country": "CN",
"region": "Hubei",
"city": "Xiangyang",
"district": "Nanzhang",
"tz": "Asia/Shanghai"
},
"network": { "asn": 4134, "usage_type": "DYN" },
"risk": {
"score": 18,
"level": "low",
"signals": ["dialup_pool", "hijacked_proxy", "proxy"]
}
}
Still DYN home broadband; signals say dial-up pool + proxy presence; hijacked_proxy warns collateral. active + low is friction territory at checkout — and the score will move as evidence ages. If you only re-check this IP at chargeback time without a stored computed_at, you may see a different story and wrongly conclude the gate was wrong.
Docs also show the same idea on Pulse history: one Luxembourg residential proxy exit scored 90 at a named past moment and 77 later — same address, same feed, evidence aged. That drift is intentional.
Minimal gate sketch (store the snapshot)
import json, urllib.request
from datetime import datetime, timezone
def lookup_now(ip: str) -> dict:
req = urllib.request.Request(f"https://ip99.com/v1/ip/{ip}")
with urllib.request.urlopen(req, timeout=5) as r:
return json.load(r)
def snapshot_for_order(ip: str) -> dict:
d = lookup_now(ip)
return {
"egress_ip": d["ip"],
"computed_at": d["computed_at"],
"evidence_state": d["evidence_state"],
"score": d["risk"]["score"],
"level": d["risk"]["level"],
"signals": d["risk"].get("signals"),
"asn": d.get("network", {}).get("asn"),
"usage_type": d.get("network", {}).get("usage_type"),
"stored_at": datetime.now(timezone.utc).isoformat(),
}
# At chargeback: compare dispute narrative to snapshot_for_order(...).
# To re-ask the feed as of order time, use Pulse ?at= with a professional key —
# do not pretend free /v1/ip can time-travel.
CORS is open on /v1/ip, but run the gate on your server. Import https://ip99.com/openapi.json into Postman or codegen if you want a typed client. Agents can call the same live lookup as MCP tool ip_risk_lookup at https://ip99.com/mcp.
Honest limits
- Score is not attacker probability;
0/evidence_state: noneis not "safe." - Free
/v1/ipanswers now. Passingatyieldsat_unsupported. - Pulse
?at=needs professional access and history enabled separately; history indexes IPv4 in the documented window (~30 days). Do not invent free historical fields. - IP99 and ThreatHunter branding: IP99 is the product; do not claim the free API and every ThreatHunter dataset are identical.
- Mask the last octet of real residential or mobile addresses in public tickets and posts.
Try it
Paste a checkout egress into ip99.com or run the free curls above. For live gates, branch on evidence_state + typed signals and store computed_at with the order. For chargebacks, argue from that snapshot — and use Pulse history when you truly need "what did the feed say at purchase," not today's re-score of a recycled address.
Top comments (1)
Replaying with the timestamp is the whole game — evaluating a past decision with present data quietly convicts people for information nobody had at the time. Keeping the decision-time snapshot, inputs and all, turns a chargeback review from an argument about memory into a check of a record, and it also protects the opposite case: a decision that looks wrong today but was correct on what was knowable then. The same discipline applies anywhere money moves on evidence — what matters in a dispute is never what the system knows now, it is what it can prove it knew when it acted.