Developers / agents
API docs
Everything on a report page is available as JSON: one call returns the score, proxy / VPN / Tor detection, blocklists and abuse history, per-use-case suitability, every deduction with its source, and the limits of the data. No API key, no sign-up. The machine-readable description is at /openapi.json; the site summary for models is at /en/llms.txt.
Endpoints
v1 endpoints, plus the legacy shapes
- GET /api/v1/ip/{ip}
- Recommended for new integrations. The report is in data; the envelope carries request_id, schema_version, checked_at, confidence, cache, score_model_version, a summary for agents, sources and unknowns. Add ?lang=en for English.
- GET /api/v1/ip/{ip}/history?limit=20
- Current report + previous checks, newest first. Read-only: no new lookup and no data-source cost. limit defaults to 20, maximum 200; has_more means older records exist. change_type separates evidence changes from score-model changes; add ?lang=en for English labels.
- GET /en/ip/{ip}
- The report as a web page, for people, search engines and AI browsing tools.
- GET /en/ip/{ip}.json
- Legacy bare report as JSON. Identical to /api/lookup?ip={ip}&lang=en — same limits, same allowance.
- GET /en/ip/{ip}.txt
- A 72-column plain-text report. No HTML or JSON parsing needed.
v1 envelope: every response carries request_id, schema_version, checked_at, confidence, cache (fresh / cache / store), score_model_version (null for reports generated before version tracking), summary (the same decision summary the MCP tools return), sources and unknowns. Errors use a stable error.code; read that instead of matching the message. The report itself is in data, without a second copy of sources / unknowns / scoreModelVersion. History items add score_model_version and change_type (evidence_change / model_change / mixed / unknown / none). If your agent browsing environment blocks /api/ paths, keep using /en/ip/{ip}.json; the data is the same.
Examples: /en/ip/8.8.8.8 · /en/ip/8.8.8.8.json · /en/ip/8.8.8.8.txt. Drop the /en prefix for the Chinese versions. Some AI browsing environments refuse API-style URLs with query parameters; the path-style addresses above are not affected, and every report page also embeds the same JSON in script#ipure-report. GET /api/myip?format=text echoes the caller's public IP.
MCP
Hosted MCP server for agents
Endpoint: https://ipure.dev/mcp (Streamable HTTP, no auth). Three tools: inspect_ip, assess_ip_for_use_case and compare_egress_ips. They return a compact summary plus report_url; use the v1 API when you need every field. Tool calls share the same gate, rate limits and unverified allowance as the HTTP API.
Claude Code
claude mcp add --transport http ipure https://ipure.dev/mcpOther MCP clients: set the URL to https://ipure.dev/mcp and use HTTP / Streamable HTTP transport. Or use this config:
mcp.json
{
"mcpServers": {
"ipure": { "type": "http", "url": "https://ipure.dev/mcp" }
}
}Limits
Who can call it, and how much
- IPs already in the database return instantly, cost nothing and can be requested by any client.
- IPs not yet in the database trigger live queries to several upstream sources. Scripts and agents get 5 such lookups per source IP per day without verification (there is also a site-wide daily total); the remainder is in the
x-open-budget-remainingheader. When it runs out the API returns 403 withcode = verification_requiredand areportUrl: open it in a browser once and the IP is in the database from then on. Site-wide, 15 unverified lookups are admitted per minute; beyond that you get 429 withcode = open_rate_limited— wait forRetry-Afterand retry. - Rate limits: 30 requests / minute; 10 first-time lookups of new IPs / minute. Over the limit returns 429.
- Freshness: stored reports carry
queriedAtand astaleflag. An IP's nature rarely changes, so a stored report is usually fine to use as is. - Bulk or scheduled checks: please get in touch via the feedback button on ipure.dev rather than scanning ranges.
Fields
What the response contains
| ip / version | Normalised address and IP version (4 / 6) |
|---|---|
| queriedAt | When the report was generated. With source = store this is the original check time; read it together with stale |
| source | fresh (live lookup) · cache (in-process) · store (database) |
| usageType | residential · mobile · business · hosting · education · government · unknown |
| nativeType | native · broadcast (registered in one country, used in another) · unknown |
| flags | isProxy / isVpn / isTor / isHosting / isMobile / isRelay / isBogon / isSearchEngineBot. Binary fields, kept for compatibility |
| flagAgreement | How many data sources agree on each flag. A single-source flag counts at half weight in the score |
| proxyAssessment | Evidence strength for proxy / VPN: status (confirmed / suspected / not_detected / unknown), kind (tor / vpn / proxy / none), confidence, positive_sources. Prefer this in summaries; never present a single-source suspected flag as confirmed |
| vpnOperator | For known commercial VPNs: name, anonymity, popularity, protocols, logging, freeAccess. Otherwise null |
| risk.purity | Purity 0-100; higher is cleaner |
| risk.level / label / verdict | Six bands: pristine · clean · neutral · suspicious · risky · dangerous, with a one-line verdict |
| risk.confidence | low / medium / high — how much data source coverage backs the score. Read score and confidence separately |
| risk.factors[] | Where every point came from: label, category, points (positive deducts, negative adds back), detail, floor (a minimum risk that cannot be offset), explanation (meaning · cares: which uses care · weighting · remedy, the same text the report page shows when a row is expanded) |
| scenarios[] | Suitability per use case: ai · social · streaming · gaming · ecommerce · email, each with score, level, levelLabel and reason. score is null when level is restricted or not_applicable |
| scenarioApplicable | false means the address is public infrastructure (a public DNS resolver, a search engine crawler), not anyone's egress IP; the reason is in scenarioNote |
| blocklists[] | Each public blocklist: listed; benign (listed but not a risk, e.g. a PBL residential declaration); unavailable (could not be queried, so unknown) |
| abuse[] | Abuse reports (score, reports) and recorded attack activity (attacks.byType: login attempts, bulk registration, vulnerability probing …). Raw reports are not confirmed abuse; score = 0 entries carry a note explaining the 0% confidence |
| exposure / sharing | Network exposure (open ports, known vulnerabilities) and sharing (devices observed on the address / in the subnet) |
| sources[] | Status of each data source for this report: ok, ms, error, skipped. A failed source is not a clean result. v1 puts this in the envelope; legacy paths put it in the report |
| feedback | Real-world experience ratings (1-5, 5 is best) per use case from visitors who were actually using this IP. Shown once a use case has 3 ratings. Independent of the score |
| unknowns[] | What cannot be determined from an IP alone — see below. Always pass these on. v1 puts this in the envelope; legacy paths put it in the report |
| reportUrl | The human-readable report. Cite it with any conclusion |
| lang / untranslated | English responses carry lang = en. untranslated lists any deduction ids that had no English text (normally empty) |
Limits of the data
What an IP check cannot tell you
Conclusions reflect public and partner data at query time. They are not a guarantee of account safety or platform access.
Each platform's internal risk labels
Platforms keep their own IP reputation data and ban records and do not publish them. No hit in public sources does not mean a platform has nothing on this IP.
Whether the IP is truly exclusive
Sharing is only the number of devices observed in the same subnet. It cannot show who else is using this exit right now, or what they are doing.
Which accounts this IP has been tied to
Account-to-IP links exist only inside each platform. Accounts previously banned on this IP are invisible to us.
Device, browser and behaviour
Risk systems look at IP + device fingerprint + behavioural rhythm together. A clean IP with an odd fingerprint or behaviour still gets blocked.
Whether the account profile matches the IP
When sign-up country, phone number, payment method, language or time zone disagree with the IP's location, platforms verify further. That is unrelated to IP purity.
Examples
Calling v1
curl · current report
curl -s "https://ipure.dev/api/v1/ip/8.8.8.8?lang=en" | jq '{id: .request_id, purity: .data.risk.purity, verdict: .data.risk.verdict, cache: .cache, sources: [.sources[] | select(.ok) | .name]}'curl · check history
curl -s "https://ipure.dev/api/v1/ip/8.8.8.8/history?limit=5&lang=en" | jq '{count, has_more, history: [.history[] | {checked_at, current, purity, level, change_type}]}'Python
import requests
r = requests.get("https://ipure.dev/api/v1/ip/8.8.8.8?lang=en", timeout=30)
r.raise_for_status()
body = r.json()
report = body["data"]
print(body["request_id"], body["cache"], report["risk"]["purity"], report["risk"]["verdict"])
if not report["scenarioApplicable"]:
print(report["scenarioNote"])
for s in report["scenarios"]:
print(s["label"], s["score"], s["levelLabel"], "-", s["reason"])
print("Cannot be determined:", [u["label"] for u in body["unknowns"]])TypeScript
const res = await fetch("https://ipure.dev/api/v1/ip/8.8.8.8?lang=en");
if (!res.ok) {
const { error } = await res.json();
throw new Error(`IPure ${error.code}: ${error.message}`);
}
const { data: report, sources, unknowns } = await res.json();
const ai = report.scenarios.find((s: { id: string }) => s.id === "ai");
console.log(report.risk.purity, ai?.levelLabel, sources.length, unknowns.length);Terminal (reports are in Chinese unless you add lang=en)
curl -L "ipure.dev/cli/8.8.8.8?lang=en&color=0"For agents
When to call it, and when not to
Good fit
“Is this IP clean?” “Can I use this proxy for ChatGPT / signing up on X / an Amazon seller account / Netflix?” “Is it a VPN or data center IP?” “Why do I keep getting flagged?” Also: checking your own egress IP before a network task, or comparing several candidate IPs.
Not a fit
DNS configuration, deployment, connectivity troubleshooting, or anything that needs a platform's internal risk data. When quoting a conclusion, pass on unknowns too, and never turn “no risk signals found” into “safe”.