The whole contract, on one page.
If you are an autonomous client, this page is for you. One GET, the exact response shape, what an absent field means as against an empty one, and the two different things a 429 can mean. Nothing here needs another page to be actionable.
The same facts are at /llms.txt as plain text, and the full reference is at docs.vpndetection.io.
One call, one address
The path is the address. IPv4 and IPv6 both, on the same endpoint. The key is a bearer token; without one you still get an answer, just a narrower one.
curl "https://api.vpndetection.io/45.83.91.1" \-H "Authorization: Bearer $VPNDETECTION_API_KEY"
GET https://api.vpndetection.io/{ip}Authorization: Bearer <key>application/json, pretty-printed200 with the body below400401429 — read Retry-AfterWhat comes back
Eight independent flags and, for each, a detail object carrying the evidence. This is a Max plan response; a lower plan omits fields rather than nulling them.
{"ip": "45.83.91.1","is_vpn": true,"is_hosting": true,"is_tor": false,"is_relay": false,"is_cdn": false,"is_resproxy": false,"is_dcproxy": false,"is_mobproxy": false,"vpn": {"provider": "mullvad","confidence": "high","method": "scan","last_seen": "2026-09-02"},"hosting": { "provider": "m247", "confidence": "high", "last_seen": "2026-09-02" },"tor": {},"relay": {},"cdn": {},"resproxy": {},"dcproxy": {},"mobproxy": {}}
is_vpnis_hostingis_toris_relayis_cdnis_resproxyis_dcproxyis_mobproxySeven things that will bite you otherwise
Each of these is a place where the obvious reading of the response is the wrong one.
A field your plan does not include is omitted from the response. Do not read a missing key as a negative answer; read it as a question you did not buy an answer to.
A flag that is present always answers. Its detail object comes back as {} when the dataset was queried and nothing matched - that is "we looked", not "we do not know".
Private, loopback, link-local, documentation and multicast ranges, including their 6to4 and Teredo forms, are answered by the client without a request. Every official SDK does this; if you call the endpoint directly, do it yourself.
Present means a transient rate limit and retrying works. Absent means a monthly allowance is spent and retrying will not help. Branch on the header, not on the status.
Retry with exponential backoff and jitter. The official clients already do.
A provider id does not change between builds, so a rule keyed on one keeps working as that operator's ranges rotate. Join on the id, not on a display name.
They are inferences from measurement, published operator data and public registration records. They can be wrong or go stale. Do not make them the sole basis for denying a person access to a service.
What a plan lets you see
Every plan queries all eight datasets. What changes is how much of the answer is returned. Full detail is on the pricing page.
When you need more than one answer
Do not loop the API over a list you already hold. Two better paths exist.
Every classification ships as a downloadable build, CSVGZ and where published MMDB, with checksums. Poll the build id and fetch only when it changes; an unchanged poll costs nothing. See the database pages.
Aggregates per block are public and need no key: every /8 down to /16 and /24, with how much of each block carries which signal. Individual address pages exist but are noindex and cost a live look-up, so crawl the blocks, not the addresses.
Start without signing up.
Paste it into a terminal — no account needed. 1k daily allowance per user, answering ip and is_vpn.
{"ip": "45.83.91.1","is_vpn": true}