Developer API
Free, no auth, CORS-enabled. Just GET the endpoints — JSON in, JSON out.
Endpoints
/check?ip={ip}Terminal ANSI output: curl -sL https://ipok.io/check formats direct plain-text report for terminal (supports ?ip=, ?plain=1, ?lang=en)
/shOne-liner shell check: curl -fsSL https://ipok.io/sh | bash executes standalone inspection on Linux/macOS (supports target IP, --json, --plain)
/api/ip?ip={ip}Full IP check: geo, ASN, IP-layer risk evidence, native label, scenario reference, and AI region snapshots
/api/ip?ip={ip}&format=textTerminal ANSI output: curl -sL https://ipok.io/check formats direct plain-text report for terminal (supports ?ip=, ?plain=1, ?lang=en)
/api/bgp?asn={asn}&ip={ip}BGP routing: announced prefix, upstream/downstream ASNs (via RIPEstat)
/api/reverse-ip?ip={ip}Reverse IP: domains hosted on the same IP
/api/batch?q={ips|cidr}Fast batch estimate using ip-api + IPOK's offline DB; not the same as the multi-source single-IP score
/api/cidr?q={cidr}Block reputation: scan IPv4 CIDRs against the offline DB (each block ≤/16; total expansion ≤65,536 addresses/request) for VPN/Tor/abuse ratios and a verdict, with no extra upstream requests
/api/badge?ip={ip}Band-colored SVG risk badge for READMEs / forums. To protect upstream capacity, badges skip Scamalytics, so the same target may score slightly differently from a user-initiated /api/ip lookup. A result using stale fallback older than 72 hours (up to 7 days) is labeled stale and colored amber. Quota exhaustion or lookup failure returns a gray n/a SVG with HTTP 200; success is cached 5 minutes and n/a 1 minute
Example
# Terminal ANSI output: curl -sL https://ipok.io/check formats direct plain-text report for terminal (supports ?ip=, ?plain=1, ?lang=en)
curl -sL "https://ipok.io/check"
curl -sL "https://ipok.io/check?ip=1.1.1.1"
# One-liner shell check: curl -fsSL https://ipok.io/sh | bash executes standalone inspection on Linux/macOS (supports target IP, --json, --plain)
curl -fsSL "https://ipok.io/sh" | bash
curl -fsSL "https://ipok.io/sh" | bash -s 1.1.1.1
# Full IP check: geo, ASN, IP-layer risk evidence, native label, scenario reference, and AI region snapshots
curl "https://ipok.io/api/ip?ip=1.1.1.1"
curl "https://ipok.io/api/ip?ip=1.1.1.1&format=text&plain=1"
# BGP routing: announced prefix, upstream/downstream ASNs (via RIPEstat)
curl "https://ipok.io/api/bgp?asn=AS13335&ip=1.1.1.1"
# Reverse IP: domains hosted on the same IP
curl "https://ipok.io/api/reverse-ip?ip=1.1.1.1"
# Fast batch estimate using ip-api + IPOK's offline DB; not the same as the multi-source single-IP score
curl "https://ipok.io/api/batch?q=1.1.1.1,8.8.8.8"
# Block reputation: scan IPv4 CIDRs against the offline DB (each block ≤/16; total expansion ≤65,536 addresses/request) for VPN/Tor/abuse ratios and a verdict, with no extra upstream requests
curl "https://ipok.io/api/cidr?q=146.70.50.0/24"
# Band-colored SVG risk badge for READMEs / forums. To protect upstream capacity, badges skip Scamalytics, so the same target may score slightly differently from a user-initiated /api/ip lookup. A result using stale fallback older than 72 hours (up to 7 days) is labeled stale and colored amber. Quota exhaustion or lookup failure returns a gray n/a SVG with HTTP 200; success is cached 5 minutes and n/a 1 minute
Key response fields
{
"apiVersion": "1", // response contract version (see Versioning below)
"geo": { "ip", "version", "country", "city", "asn", "asName", "isp", "lat", "lon" },
"ipType": "residential | hosting | mobile | business | unknown",
"usageType": "isp | dual_isp | business | hosting | mobile | education | government | unknown",
"risk": 0, // compatibility value; display only if scoreAvailable=true
"riskAssessment": {
"version": 1,
"status": "insufficient | limited | current",
"scoreAvailable": false,
"safeConclusion": false,
"independence": "unverified",
"currentSources": [], "staleSources": [], "undatedSources": [], "invalidSources": [],
"reasons": [], "assessedAt": "2026-10-20T05:00:00Z"
},
"riskBreakdown": { // how the score was computed (see /methodology)
"contributors": [{ "source", "risk", "weight" }],
"weightedAvg": 0,
"floors": [{ "key": "tor", "floor": 90 }],
"final": 0
},
"signals": ["proxy", "hosting", ...],
"blocklist": { "listed": ["SpamCop"], "checked": 5, "total": 5 }, // valid DNSBL results / planned lists, IPv4 only
"anonAsn?": { "asn", "name", "type" }, // known VPN/proxy provider ASN
"nativeType": "native | broadcast | unknown", // legacy enum: registration/location agreement only
"sharedUsers": { "range": "1-5", "quality": "good | ok | poor | unknown" },
"scenarios?": [{ "key": "tiktok", "stars": 3, "verdict": "try" }], // omitted when evidence is insufficient
"services": [{ "key": "chatgpt", "status": "available" }],
"media": [{ "key": "netflix", "status": "unknown" }], // known-region restriction reference, not a live unlock test
"sources": [{ "source": "ip-api", "risk": 10, "flags": {}, "observedAt": "2026-09-23T04:15:00Z" }],
"sourceAttempts?": [{ "source": "IPQS", "status": "not_called", "reason": "dormant", "elapsedMs": 0, "upstream": "not_called" }],
"diagnostics?": { "timings": { "totalMs": 600, "geoMs": 100, "sourcesMs": 400, "cacheMs": 10, "admissionMs": 50, "quotaReadMs": 10, "requestMs": 660 } },
// observedAt = when this source was read: the time of this
// request on a live fetch, or the time the cache entry was written
"evidence?": { // why the verdict landed where it did
"dedicatedVotes", "voters": [], "floors": [], "directSources": [],
"confidence": "none | single | multi", "singleSource?"
},
"attribution?": { "provider?", "datacenter?", "company?", "asnOrg?" }, // who the address belongs to
"intel?": { "asn", "company", "datacenter", "abuse" }, // ipapi.is enrichment
"asnHistory?": [{ "asn", "firstSeen", "lastSeen" }],
"rdap?": { "registry", "country", "registered", "org" }, // "?" = optional, omitted when unavailable
"quota?": { // your remaining fair-use budget (see Quota below)
"apiVersion": "1",
"selfLookup?": { "used", "limit", "remaining", "resetsInSeconds" },
"arbitraryLookup?": { "used", "limit", "remaining", "resetsInSeconds" },
"note"
}
}Use format=text explicitly for a terminal report; HTML Accept alone does not change JSON. Text with stream=1 or event-stream negotiation is rejected with JSON 400 before admission. /check uses the same admission and quota policy. fast=1 or quick=1 still requires admission and then reads available cache/free evidence without commercial fetches. Errors keep their existing JSON contract on /api/ip.
riskAssessment is evaluated again at response time. When scoreAvailable=false, hide the aggregate score and scenario ratings; the legacy numeric risk remains for compatibility, not as a current conclusion. Source scores and positive observations remain visible with their dates. The 72-hour source age bound follows existing cache retention, and the offline snapshot bound is 10 days; neither is an accuracy guarantee. Distinct vendor names do not establish independent data collection. Missing evidence is not safety, and there is no invented four-source minimum.
IPOK-DB raw.sourceStatus reports coverage, freshness, match, fetchedAt, publishedAt and version for each source and address family. unsupported/unavailable/unknown do not mean no risk; no_match means only no match in that snapshot. Cloud ranges and Apple relay membership are network context, not abuse observations. Cloudflare Anycast network membership does not identify a WARP exit, product or physical location. nativeType is a legacy country-agreement enum and is not proof of a native exit.
Geolocation starts with ip-api; if unavailable, it falls back to IPinfo Lite, then ipapi.is. IPinfo Lite supplies country and ASN information only: it does not produce a risk verdict or enter scoring or consensus votes. City, coordinates and other detailed fields may be absent.
sourceAttempts covers all registered sources, including those that did not vote. status is success, unavailable or not_called. Reasons distinguish dormant, policy_skip, quota, cached_only, cache_miss, unconfigured, upstream_error and no_evidence. Missing evidence is never a clean verdict. If an adapter does not expose a failure cause, upstream_error does not claim a timeout.
cache is l1, l2, stale or miss, separate from the latest upstream attempt. A successful stale fallback can still carry reason=quota. elapsedMs, cacheElapsedMs and upstreamElapsedMs measure this request; topup marks a supplemental check. diagnostics.timings and Server-Timing report server measurements in ms. Cache time is summed across concurrent operations and must not be added to phase wall times. requestMs excludes client network/edge transit and response serialization; compare it with your own total latency.
App, extension and public API calls need no CAPTCHA. Unproved clients share a common hourly pool in addition to per-IP, class and vendor limits; changing headers does not create another common pool. Missing quota infrastructure blocks commercial fetches while self-lookups retain free sources/cache. Supplier account credits are private operational metadata and are removed from raw; documented risk, flags and source verdict fields remain unchanged.
Status / errors
| 200 | OK |
| 400 | Bad request: invalid/private IP, missing ip/asn, etc. |
| 429 | Minute or daily quota exhausted; retry after the supplied interval |
| 503 | Quota service or batch upstream temporarily unavailable |
| 502 | Upstream data source unavailable or timed out |
Rate limits & caching
Limits are keyed by requester IP (IPv6 grouped to /64). Only /api/ip has an additional 30/minute burst gate within each Worker instance; MCP, /api/ip, and /api/badge share daily buckets of 20 self checks per UTC day and 15 target-IP lookups/day. Plain curl/CLI calls directly to /api/ip beyond the target daily bucket continue with cached commercial data and free sources; MCP and other client classes retain their daily limits. Badge exhaustion returns a gray n/a instead of 429. Batch allows 25 upstream chunks/day (up to 100 targets per chunk); reverse-ip, whois, and BGP each allow 25 requests/UTC day, CIDR allows 10/day, and the DNS-leak proxy allows 40 HTTP requests/day (about 20 complete tests). One CIDR request may expand at most 65,536 addresses. Scamalytics is attempted only for explicit cache misses under three caps: five tries/requester/UTC day, six site-wide tries/UTC hour, and the upstream monthly allowance. Exhausted capacity uses a visibly marked fallback cache when available; otherwise that source is omitted and the full lookup continues. Commercial-source fetches (AbuseIPDB / proxycheck / ipapi.is / Scamalytics) are pooled per client class and UTC hour, with separate pools for self checks and target-IP lookups: target lookups from this site's pages that pass Cloudflare Turnstile stay out of the pool; a verified self-check (Turnstile or Proof-of-Work, no target or target equal to the source IP) uses a separate site-wide commercial bucket (default 4000/UTC day, distinct from the class pools, then cached-only); unverified self-checks stay on the class pools; Proof-of-Work-only target lookups still use the 120 / 30 pool; the official app gets 120 / 60, browser extensions 120 / 30, honest programmatic clients (curl, scripts, MCP) 100 / 30, cross-site calls from other web apps 60 / 30, direct browser navigations to the endpoint 30 / 10, and requests that claim to be a browser but lack expected headers 20 / 5. The last category also needs a valid PoW or session for a non-self target, otherwise it gets 403 with x-ipok-pow-required: 1. All classes without Turnstile proof additionally share a common hourly pool of 120 self / 60 target lookups. A spent pool never rejects: the commercial sources are served only from cache or clearly marked stale values with no upstream fetch, the free sources still participate, and the response carries upstream:"cached-only" plus x-ipok-upstream and x-ipok-client headers. Requests carrying header combinations no real browser produces (a navigation with XHR-only headers, a page fetch with navigation-only headers, a User-Agent OS that contradicts the client hints) are rejected with 403; an honest HTTP client is all you need. Matching cache hits do not consume auxiliary quotas. Limits return 429 except for the CLI and badge fallbacks above; a production quota-service failure returns 503. Add your own cache and contact us for higher-volume use.
Quota Block
Successful /api/ip responses carry a quota block so you can see the remaining budget for your source IP without waiting for a 429. selfLookup covers checking your own address (no ?ip=); arbitraryLookup covers explicit ?ip= queries. Each carries used, limit, remaining and resetsInSeconds, counted per source IP and reset at the UTC day boundary. IPv6 addresses are counted per /64, not per address.
The same two numbers are repeated in the X-IPOK-Quota-Self-Remaining and X-IPOK-Quota-Lookup-Remaining response headers, so a client can read them without parsing the body. The counter is read after the current request has been charged, so used already includes it. Plain HTTP target requests served after the daily budget is exhausted use cached commercial results and do not increment that counter.
A sub-block is omitted when its counter cannot be read. Read that as unknown, not as unlimited — the limit still applies.
These per-IP budgets are not the only limit. The commercial risk sources also sit behind a site-wide hourly pool; when it is full the response is still complete, but those sources serve cached or recent values only and the result is marked upstream: "cached-only" (header x-ipok-upstream). Zero-quota sources take part as usual.
Versioning
Send Accept-Version: 1 to pin the response contract. The version actually served is echoed in the X-IPOK-API-Version response header and repeated in the body as apiVersion. A version we do not recognise is not an error: we still serve v1 and say so in X-IPOK-API-Warning. Omitting the header also gets v1.
The policy has two rules. Adding a new optional field does not bump the version, so parse defensively and ignore fields you do not know. Changing what an existing field means, or removing one, does bump it — and the old version keeps being served to clients that ask for it.
Newly added fields
These are optional additions, so the version stays at 1. Older clients that ignore unknown fields are unaffected.
evidence — what the verdict rests on: dedicatedVotes (how many dedicated proxy/VPN sources flagged the address), voters (which ones), floors (score floors that were triggered), directSources, and confidence — none, single or multi. With single, singleSource names the only source that flagged it; a single vote is weaker evidence than several, and this field is there so you can treat it that way.
attribution — who the address belongs to, as far as we can tell: provider, datacenter, company, asnOrg. Each is present only when a source supplied it.
sources[i].observedAt — when that source was read, as an ISO timestamp: the time of this request on a live fetch, or the time the cache entry was written on a cache hit. Together with the cached-only marker above, this is how you tell fresh readings from stale ones.
Attribution Policy
Our API is free and does not require registration. However, if you display, quote, or reference our IP risk intelligence data (such as IP type, threat scores, or VPN flags) in your public products, web pages, or research, you must prominently credit IPOK as the data source and include an active, followable HTML backlink to https://ipok.io.
Please use responsibly; add your own cache for high-volume or commercial use.