The whole contract, on one page.
If you're an autonomous client, this page is for you: the one GET, the exact response shape, what an absent field means next to an empty one, and the two things a 429 can mean. You won't need another page to act on it.
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/146.70.116.130" \-H "Authorization: Bearer $VPNDETECTION_API_KEY"
GET https://api.vpndetection.io/{ip}Authorization: Bearer <key>application/json, pretty-printed200 with the body below400401403429, read Retry-AfterWhat comes back
Nine independent flags and, for each, a detail object carrying the evidence, then where the address is and which network announces it. Every paid plan answers this; without a key or on Free, the fields that answer leaves out are omitted rather than nulled.
{"ip": "146.70.116.130","is_vpn": true,"is_hosting": true,"is_relay": false,"is_tor": false,"is_cdn": false,"is_resproxy": false,"is_dcproxy": false,"is_mobproxy": false,"is_pubproxy": false,"vpn": {"provider": "mullvad","last_seen": "2026-09-02","confidence": "high"},"hosting": { "provider": "m247", "confidence": "high", "last_seen": "2026-09-02" },"relay": {},"tor": {},"cdn": {},"resproxy": {},"dcproxy": {},"mobproxy": {},"pubproxy": {},"location": {"continent": "EU","continent_name": "Europe","country": "AT","country_name": "Austria","timezone": "Europe/Vienna","currency_code": "EUR","currency_name": "Euro","currency_symbol": "€","flag_emoji": "🇦🇹","flag_unicode": "U+1F1E6 U+1F1F9"},"asn": { "asn": 9009, "name": "M247 Europe SRL", "domain": "m247global.com", "type": "hosting", "prefix": "146.70.116.0/24" }}
is_vpnis_hostingis_toris_relayis_cdnis_resproxyis_dcproxyis_mobproxyis_pubproxylocation.continent, continent_namelocation.country, country_namelocation.timezonelocation.currency_code, currency_name, currency_symbollocation.flag_emoji, flag_unicodeasn.asnasn.nameasn.domainasn.typeasn.prefixSeven 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 doesn't include is omitted from the response. Don't read a missing key as a negative answer; read it as a question you didn't 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: "we looked", not "we don't know".
Private, loopback, link-local, documentation and multicast ranges, including their 6to4 and Teredo forms, answer 200 with every flag false. Every official SDK answers them without a request. Sent to the API directly, they count against a keyless or Free plan allowance and cost nothing on a paid plan. Every range is listed at /bogon.
Present means a transient rate limit and retrying works. Absent means a monthly allowance is spent and retrying won't help. Branch on the header, not on the status.
Retry with exponential backoff and jitter. The official clients already do.
A provider id doesn't 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. Don't make them the sole basis for denying a person access to a service.
What a plan lets you see
What an answer carries depends on the key, and every paid plan answers the same: the plans differ in lookups alone. Full detail is on the pricing page.
When you need more than one answer
Don't loop the API over a list you already hold. Use one of these instead.
POST https://api.vpndetection.io/batch answers up to 1,000 addresses at once, each costing what its single lookup costs, keyed by the string you sent. See the batch call.
Every classification ships as a downloadable build, CSVGZ and where published MMDB, with checksums. Poll its metadata and fetch again only when it reports a newer build date. 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. 1,000 lookups a day per address, answering ip and is_vpn.
{"ip": "146.70.116.130","is_vpn": true}