URL: /products/ip-intelligence

---
title: IP Intelligence
description: "IP reputation API: GET one IP and get a 0-100 risk score, class + risk tags (datacenter, VPN, proxy, Tor, scanner), threat-blocklist hits (Spamhaus, botnet C2), ASN, geo, and city."
---

A single IP reputation API call returns a **0-100 risk score** and the address's class and risk tags — datacenter, VPN, proxy, Tor, residential proxy, scanner — plus threat-blocklist hits (Spamhaus DROP, botnet C2, abuse reporters), ASN, geo, and optional company, reverse-DNS, and city enrichment.

You can see the IP on every request, but a bare address tells you nothing about whether it's a home connection or a rented VPS running a scanner. Stitching together a geo database, an ASN feed, and a proxy/VPN list yourself means maintaining three data sources and still missing residential proxies and fresh Tor exits. IP Intelligence collapses that into one authenticated `GET`.

## When to use it

Reach for `GET /v1/ip/{ip}` whenever you have an address and want to know what kind of network it belongs to before you trust it — at signup, on a login, gating a checkout, or enriching a log line. A datacenter- or cloud-classed IP hitting a consumer signup form is rarely a real customer; a Tor or residential-proxy tag on a payment attempt is worth a second look.

It complements the [beacon](/quickstart) and [server reporting](/guides/server-reporting), which score full requests. IP Intelligence is the narrow, synchronous lookup you call yourself when all you have — and all you need — is the address.

## How it works

<Steps>
  <Step title="Send the IP">
    Make one authenticated `GET` to `/v1/ip/{ip}` with your Bearer key. Works for any IPv4 or IPv6 address — the one you pulled from a request header, a signup record, or a log line. No request body, no batching ceremony.
  </Step>

  <Step title="We resolve it against a compiled blob">
    The lookup is a longest-prefix match against a single self-hosted dataset loaded in-memory at the edge — no third-party round trip. It merges ASN, geo, datacenter/proxy/VPN/Tor/scanner classification, and public threat blocklists (Spamhaus DROP, abuse.ch Feodo, blocklist.de, stamparm/ipsum, Team Cymru bogons) offline, so a typical lookup resolves in about a millisecond.
  </Step>

  <Step title="You get a score, tags, and risk back">
    The response groups everything: a **0-100 `risk.score`** with a `level` band, `network` (asn, org), `location` (ISO country, plus city via `?include=city`), `type` (datacenter/hosting/isp/mobile/cloud), and `risk` (anonymity + threat-blocklist flags), plus a flat `flags` array. Threshold on the score, or branch on the specific booleans you care about.
  </Step>
</Steps>

## Quickstart

Send an authenticated `GET` to `/v1/ip/{ip}`. No body, no headers beyond `Authorization`.

```bash
curl https://api.formshield.dev/v1/ip/185.220.101.1 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Every response is wrapped in the standard [envelope](/api-reference/introduction#response-envelope) — the answer in `data`, request info in `metadata`. This is a known Tor exit node, so it scores `100` / `high` and carries the full set of risk tags:

```json
{
  "version": "1",
  "data": {
    "ip": "185.220.101.1",
    "ip_version": 4,
    "network": { "asn": 60729, "org": "TORSERVERS-NET" },
    "location": { "country": "DE", "city": null },
    "type": { "datacenter": true, "hosting": true, "isp": false, "mobile": false, "cloud": false },
    "risk": {
      "proxy": true, "vpn": true, "tor": true, "residential_proxy": false, "scanner": true,
      "spamhaus_drop": false, "feodo_c2": false, "blocklist_de": false, "bogon": false, "blocklist": false, "ipsum_level": 0,
      "score": 100, "level": "high",
      "factors": ["tor", "proxy", "vpn", "scanner", "datacenter", "hosting"]
    },
    "flags": ["datacenter", "hosting", "proxy", "vpn", "tor", "scanner"]
  },
  "error": null,
  "metadata": {
    "request_id": "req_04e3c0cc2ffd",
    "processing_time_ms": 1,
    "source": "ipatlas",
    "dataset": "ipatlas-v4-2026-06-08-bae459220a",
    "format_version": 2,
    "enrichment": { "requested": [], "resolved": [], "credits": 1 }
  }
}
```

The base lookup costs **one credit** and resolves in about a millisecond. The simplest integration thresholds on `risk.score` (e.g. `>= 60` → challenge); for finer control, branch on the specific booleans — treat `risk.tor` or `type.datacenter` on a consumer signup as a reason to challenge or review. `metadata.dataset` is the exact dataset build that answered, so a verdict is always traceable.

## The fields

Each lookup returns the same grouped shape. The two groups that matter most are `type` (what the network *is*) and `risk` (how the IP is *being used*).

<ParamField path="network" type="object">
  The owning network: `asn` (autonomous system number) and `org` (the registered org name). A datacenter or cloud IP hitting a consumer signup form is rarely a real customer.
</ParamField>

<ParamField path="location" type="object">
  `country` — the ISO country code for the address. City-level fields (`city`, `subdivision_name`, `latitude`, `longitude`) populate only when you ask for [`?include=city`](#enrichment-with-include).
</ParamField>

<ParamField path="type" type="object">
  Five booleans describing the kind of network: `datacenter`, `hosting`, `isp`, `mobile`, `cloud`. They aren't mutually exclusive — a hosting IP is usually `datacenter` too.
</ParamField>

<ParamField path="risk" type="object">
  The threat axis. A **`score`** (0-100, higher = riskier) and **`level`** band (`none`/`low`/`medium`/`high`) summarize everything; `factors` lists what moved the score (see [Reputation score](#reputation-score)). The booleans break it down: anonymity/abuse tells — `proxy`, `vpn`, `tor`, `residential_proxy`, `scanner` — and threat-blocklist hits — `spamhaus_drop` (hijacked/spam netblock), `feodo_c2` (active botnet command-and-control), `blocklist_de` (recently reported for abuse), `bogon` (unrouted/spoofed source), `blocklist` (multi-list consensus), plus `ipsum_level` (0-8, how many lists agree). These threat flags are **positive-only**: `true` is high-confidence, but `false` just means "not on our lists," not "proven clean."
</ParamField>

<ParamField path="flags" type="string[]">
  A flat array of every `type` and `risk` flag that is `true` (the booleans only — not the `score`/`level`/`ipsum_level` scalars). Convenient when you'd rather scan one list than read each boolean.
</ParamField>

<Note>
  One flag is **positive**: `icloud_relay` marks an [Apple iCloud Private Relay](https://support.apple.com/en-us/102602) egress — a real, signed-in Apple user behind a privacy relay, not anonymising infrastructure. It can sit alongside `datacenter`/`cloud` (the relay egresses through Apple's CDN partners), but treat it as a trust signal: a relay address is a high-value consumer, not a rented server.
</Note>

### Type vs risk at a glance

| Group | Fields | Answers |
| --- | --- | --- |
| `type` | `datacenter`, `hosting`, `isp`, `mobile`, `cloud` | What kind of network owns this IP? |
| `risk` | `score` + `level`; `proxy`, `vpn`, `tor`, `residential_proxy`, `scanner`; `spamhaus_drop`, `feodo_c2`, `blocklist_de`, `bogon`, `blocklist`, `ipsum_level` | How risky is this IP, and why? |

An address can carry several of both at once, so branch on the specific booleans your use case cares about rather than assuming one excludes another.

## Reputation score

`risk.score` is a single **0-100** number — **0 is clean, 100 is maximum risk** — so you can gate on one threshold instead of reading a dozen booleans. It's always present, costs no extra credit, and comes with a `level` band and a `factors` audit trail.

| `level` | `score` | Typical handling |
| --- | --- | --- |
| `none` | `0` | Allow |
| `low` | `1`–`29` | Allow / log |
| `medium` | `30`–`59` | Review or step-up |
| `high` | `60`–`100` | Challenge or block |

The score is **additive and transparent**: each signal contributes a weight, and `factors` names exactly what moved it, so you can see *why* an address scored the way it did. Threat-blocklist hits weigh heaviest (a `spamhaus_drop` or `feodo_c2` alone lands in `medium`/`high`); anonymity tells (`tor`, `proxy`, `residential_proxy`) add moderate risk; a residential `isp` and `icloud_relay` *subtract* risk. A **verified crawler** (a `bot` block) cancels the datacenter penalty — a real Googlebot from a datacenter isn't risky.

```bash
curl https://api.formshield.dev/v1/ip/1.10.16.1 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "data": {
    "ip": "1.10.16.1",
    "ip_version": 4,
    "location": { "country": null },
    "type": { "datacenter": false, "hosting": false, "isp": false, "mobile": false, "cloud": false },
    "risk": {
      "proxy": false, "vpn": false, "tor": false, "residential_proxy": false, "scanner": true,
      "spamhaus_drop": true, "feodo_c2": false, "blocklist_de": false, "bogon": false, "blocklist": false, "ipsum_level": 0,
      "score": 90, "level": "high",
      "factors": ["spamhaus_drop", "scanner"]
    },
    "flags": ["scanner", "spamhaus_drop"]
  }
}
```

<Note>
  The weights are calibrated, not contractual — treat `score` as a tunable signal, branch on `level` for stable bands, and read `factors` when you need to explain a decision. The exact integer can shift as the model is retuned; the band thresholds are stable.
</Note>

## Enrichment with `?include=`

Default lookups stay lean and cheap. Opt into extra datasets with a comma-separated `?include=` (or `include=all`). Unknown names return a `400`.

| dataset | adds | cost |
| --- | --- | --- |
| `region` | `network.cloud_provider` + `network.cloud_region` (e.g. `aws` / `us-east-1`) | included |
| `company` | a `company` block: `{ name, website, country, category, confidence }` (by ASN) | included |
| `rdns` | `network.hostname` (reverse DNS / PTR) + `network.hostname_verified` (forward-confirmed) | +1 credit on a cache miss |
| `city` | `location.city`, `subdivision_name`, `latitude`, `longitude` (IPv4 only) | included |

```bash
curl "https://api.formshield.dev/v1/ip/3.5.1.1?include=region,company,rdns" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "data": {
    "ip": "3.5.1.1",
    "ip_version": 4,
    "network": {
      "asn": 16509,
      "org": "AMAZON-02",
      "hostname": "s3-us-east-1.amazonaws.com",
      "hostname_verified": true,
      "cloud_provider": "aws",
      "cloud_region": "us-east-1"
    },
    "company": {
      "name": "Amazon.com",
      "website": "https://www.amazon.com",
      "country": "US",
      "category": "Hosting and Cloud Provider",
      "confidence": "high"
    },
    "location": { "country": "US" },
    "type": { "datacenter": true, "hosting": true, "isp": false, "mobile": false, "cloud": true },
    "risk": {
      "proxy": false, "vpn": false, "tor": false, "residential_proxy": false, "scanner": false,
      "spamhaus_drop": false, "feodo_c2": false, "blocklist_de": false, "bogon": false, "blocklist": false, "ipsum_level": 0,
      "score": 23, "level": "low",
      "factors": ["datacenter", "hosting", "cloud"]
    },
    "flags": ["datacenter", "hosting", "cloud"]
  },
  "error": null,
  "metadata": {
    "source": "ipatlas",
    "enrichment": {
      "requested": ["region", "company", "rdns"],
      "resolved": ["region", "company", "rdns"],
      "cache": { "rdns": "hit" },
      "credits": 1
    }
  }
}
```

<Note>
  Every response reports `metadata.enrichment` with `requested`, `resolved`, and the exact `credits` charged, so cost is never a surprise. One credit per lookup; the only enrichment that can cost extra is `rdns`, and only on a cache miss (`+1`).
</Note>

### City geolocation

`?include=city` adds city, region, and coordinates to the `location` block:

```bash
curl "https://api.formshield.dev/v1/ip/1.0.0.5?include=city" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "data": {
    "ip": "1.0.0.5",
    "ip_version": 4,
    "location": {
      "city": "South Brisbane",
      "subdivision_name": "Queensland",
      "latitude": -27.4767,
      "longitude": 153.017,
      "country": "AU"
    }
  },
  "metadata": { "enrichment": { "requested": ["city"], "resolved": ["city"], "credits": 1 } }
}
```

<Note>
  City data is **IPv4-only** today; for an IPv6 address the city fields stay `null`. City accuracy is approximate (~city-level, lower for mobile networks). City geolocation by [DB-IP](https://db-ip.com).
</Note>

## Known bots

When the address falls inside a **verified crawler's published IP range**, the response carries a `bot` block — **independent of the User-Agent**. This identifies a crawler hitting you with no UA, or one forging a browser UA, from the IP alone. No `bot` key means the address isn't in a known crawler range (it may still be a bot the UA reveals — that's a separate check).

```bash
curl https://api.formshield.dev/v1/ip/66.249.66.1 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "data": {
    "ip": "66.249.66.1",
    "network": { "asn": 15169, "org": "GOOGLE" },
    "type": { "datacenter": true, "hosting": false, "isp": false, "mobile": false, "cloud": false },
    "bot": {
      "is_known_bot": true,
      "operator": "Google",
      "name": "Googlebot",
      "id": "googlebot",
      "verified_method": "published_range"
    }
  }
}
```

<ParamField path="bot" type="object">
  Present only when the IP is in a known crawler range. `operator` is the company (e.g. `Google`, `OpenAI`, `Amazon`), `name` + `id` are the crawler, and `verified_method` is how the IP was matched (`published_range`). Covers Googlebot, Bingbot, GPTBot, Applebot, Amazonbot, DuckDuckBot, and more — IPv4 and IPv6.
</ParamField>

<Note>
  Where an operator's crawlers share IP ranges (OpenAI's GPTBot / ChatGPT-User / SearchBot), the `bot` block reports the **operator** with a representative crawler name — on a bare IP you can know the operator but not always the exact product. The `bot` block is free (no extra credit).
</Note>

## IPv6

IPv6 resolves through the same endpoint, envelope, and field shape as IPv4 — the only difference is `ip_version: 6`. IPv6 is classified at **/64 granularity** (the standard single-network boundary): reputation is constant within a `/64`, so the host bits don't change the answer. `?include=` works the same; `company` resolves by ASN, so it's populated for IPv6 too.

```bash
curl "https://api.formshield.dev/v1/ip/2606:4700:4700::1111?include=company" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "data": {
    "ip": "2606:4700:4700::1111",
    "ip_version": 6,
    "network": { "asn": 13335, "org": "CLOUDFLARENET", "cloud_provider": null, "cloud_region": null },
    "company": { "name": "Cloudflare", "website": "https://www.cloudflare.com", "country": "US", "category": "Hosting and Cloud Provider", "confidence": "high" },
    "location": { "country": "US", "city": null },
    "type": { "datacenter": true, "hosting": true, "isp": false, "mobile": false, "cloud": true },
    "risk": {
      "proxy": false, "vpn": false, "tor": false, "residential_proxy": false, "scanner": false,
      "spamhaus_drop": false, "feodo_c2": false, "blocklist_de": false, "bogon": false, "blocklist": false, "ipsum_level": 0,
      "score": 23, "level": "low",
      "factors": ["datacenter", "hosting", "cloud"]
    },
    "flags": ["datacenter", "hosting", "cloud"]
  },
  "error": null,
  "metadata": { "source": "ipatlas", "enrichment": { "requested": ["company"], "resolved": ["company"], "credits": 1 } }
}
```

A v4-mapped address (`::ffff:a.b.c.d`) is folded to its IPv4 form: the response echoes the dotted quad with `ip_version: 4`.

## Errors

A non-2xx populates `error` (and `data` is `null`); these responses are **not billed**. The `message` text is human-readable but not stable — branch on `error.code`.

| Status | `error.code` | When | What to do |
| --- | --- | --- | --- |
| `400` | `VALIDATION_ERROR` | The path isn't a valid IPv4/IPv6 address, or `?include=` names an unknown dataset | Fix the address; use only `region`, `company`, `rdns`, `city`, or `all` |
| `422` | `UNSUPPORTED` | A reserved / non-global address — private, loopback, link-local, ULA (`fc00::/7`), or multicast — which carries no public intelligence | Look up a public, routable address |
| `404` | `NOT_FOUND` | The address is valid but unclassified (an unallocated IPv6 `/64`, or the dataset isn't loaded) | Nothing to fix — there's simply no record |

```json
{
  "version": "1",
  "data": null,
  "error": { "code": "VALIDATION_ERROR", "message": "unknown include dataset(s): bogus" },
  "metadata": { "request_id": "req_8f2a1c", "processing_time_ms": 0 }
}
```

## Common questions

**How do I check an IP's reputation with the API?**

Send an authenticated `GET` to `https://api.formshield.dev/v1/ip/{ip}` with an `Authorization: Bearer` header. You get back a 0-100 `risk.score` with a `level` band, the IP's network class (datacenter, hosting, isp, mobile, cloud), its risk tags (proxy, vpn, tor, residential_proxy, scanner) and threat-blocklist hits (Spamhaus DROP, botnet C2, abuse reporters, bogon), ASN, org, and ISO country — in one response. No request body is needed, and each lookup costs one credit.

**What is the `risk.score`, and how should I use it?**

It's a single 0-100 number (0 = clean, 100 = max risk) so you can gate on one threshold instead of reading every boolean. Branch on the `level` band for stability — `none`/`low` → allow, `medium` → review, `high` → challenge or block — and read `factors` to see which signals drove it. The score is included on every lookup at no extra credit. See [Reputation score](#reputation-score).

**What's the difference between the `type` flags and the `risk` flags?**

The `type` block describes what kind of network owns the IP — datacenter, hosting provider, ISP, mobile carrier, or cloud. The `risk` block scores and flags how the IP is being used in ways that correlate with abuse: anonymity tells (proxy, VPN, Tor, residential proxy, scanner) and threat-blocklist hits (Spamhaus DROP, botnet C2, abuse reporters, bogon). An address can carry several of both at once, so branch on the specific booleans your use case cares about — or just threshold the `score`.

**How much does each lookup cost, and what costs extra?**

A base lookup is one credit, and it includes the `risk.score`. The `region`, `company`, and `city` enrichments add no extra credit. The only add-on that can cost more is `rdns` (reverse DNS), which adds one credit only when the hostname isn't already cached. Every response reports the exact credits charged under `metadata.enrichment`.

**Does it support IPv6?**

Yes. `GET /v1/ip/{ip}` resolves IPv6 with the same envelope, score, and fields as IPv4 (`ip_version: 6`), classified at /64 granularity. `?include=region,company,rdns` works for IPv6 too — `company` is keyed by ASN, and reverse DNS uses `ip6.arpa`. The one exception is `?include=city`, which is **IPv4-only** for now (city fields stay `null` for IPv6).

## Next steps

<CardGroup cols={2}>
  <Card title="API reference" icon="code" href="/api-reference/introduction">
    The full envelope, enrichment, and error responses for `GET /v1/ip/{ip}`.
  </Card>
  <Card title="Server reporting" icon="server" href="/guides/server-reporting">
    Score full requests — UA and IP together — to capture crawlers and AI bots.
  </Card>
  <Card title="Bot detection" icon="bot" href="/guides/bot-detection">
    Name and IP-verify crawlers, and allow or block them per project.
  </Card>
  <Card title="Quickstart" icon="rocket" href="/quickstart">
    Get scored pageviews flowing into your dashboard in minutes.
  </Card>
</CardGroup>
