# REST API Reference

> Complete reference for the IPScope v1 REST API service: single & batch IP lookups, proxy-aware caller detection, CIDR prefix inspection, dataset metadata, feedback submission, and signed MMDB artifact distribution.

The IPScope REST API (`@ipscope/api`) is a low-latency NestJS + Fastify service served over HTTPS (HTTP/2 and HTTP/3) from `https://api.ipscope.io`. Lookups resolve in sub-millisecond memory against an atomic Radix/LPM routing snapshot, with automatic live BGP, RIPEstat, and RDAP `#geofeed` enrichment on cache misses.

## Quick request & response explorer

Select your preferred language tab below (`curl`, `Python`, `Node.js`, `Go`, `Rust`, `Java`, `.NET`, `Ruby`, `PHP`). Your selection is automatically remembered across every page in the documentation.

```bash curl
curl -sS "https://api.ipscope.io/v1/ip/198.51.100.7" \
  -H "Authorization: Bearer $IPSCOPE_KEY"
```

```python Python
import asyncio, os
from ipscope import AsyncIpScopeClient

async def main() -> None:
    async with AsyncIpScopeClient(api_key=os.environ["IPSCOPE_KEY"]) as client:
        record = await client.lookup_ip("198.51.100.7")
        print(record["ip"], record["geo"]["country_code"], record["security"]["is_vpn"])

asyncio.run(main())
```

```ts Node.js
import { IpScopeClient } from '@ipscope/sdk-node';

const client = new IpScopeClient({ apiKey: process.env.IPSCOPE_KEY });
const record = await client.lookupIp('198.51.100.7');
console.log(record.ip, record.geo?.country_code, record.security?.is_vpn);
```

```go Go
client := ipscope.NewClient(ipscope.Config{APIKey: os.Getenv("IPSCOPE_KEY")})
defer client.Close()

record, err := client.LookupIP(ctx, "198.51.100.7")
if err != nil {
	return err
}
fmt.Println(record.IP, record.Security.IsVPN)
```

```rust Rust
use ipscope::{AsyncIpScopeClient, ClientConfig};

let client = AsyncIpScopeClient::new(ClientConfig::new(std::env::var("IPSCOPE_KEY")?))?;
let record = client.lookup_ip("198.51.100.7").await?;
println!("{} VPN={}", record.ip, record.security.is_vpn);
```

```java Java
import io.ipscope.sdk.IpScopeClient;
import io.ipscope.sdk.IpScopeConfig;

IpScopeConfig config = IpScopeConfig.builder().apiKey(System.getenv("IPSCOPE_KEY")).build();
try (IpScopeClient client = new IpScopeClient(config)) {
    var record = client.lookupIp("198.51.100.7");
    System.out.println(record.ip() + " " + record.geo().countryCode());
}
```

```csharp .NET
using IpScope.Sdk;

using var client = new IpScopeClient(new IpScopeClientOptions {
    ApiKey = Environment.GetEnvironmentVariable("IPSCOPE_KEY")
});
var record = await client.LookupIpAsync("198.51.100.7");
Console.WriteLine($"{record.Ip} {record.Geo?.CountryCode} VPN={record.Security?.IsVpn}");
```

```ruby Ruby
require "ipscope"

client = IpScope::Client.new(api_key: ENV.fetch("IPSCOPE_KEY"))
record = client.lookup_ip("198.51.100.7")
puts "#{record.ip} #{record.geo.country_code} VPN=#{record.security.is_vpn}"
```

```php PHP
<?php
use IpScope\Sdk\IpScopeClient;

$client = new IpScopeClient(apiKey: getenv('IPSCOPE_KEY') ?: '');
$record = $client->lookupIp('198.51.100.7');
echo $record->ip . " " . $record->geo->countryCode . PHP_EOL;
```

### 200 `IpLookupResponse`: Resolved IPv4/IPv6 intelligence record projected to your key’s licensedFamilies.

- `ip` (string, required): Canonical normalized representation of the queried IPv4 or IPv6 address.
- `ip_version` (integer, required): Protocol family (`4` for IPv4, `6` for IPv6).
- `matched_network` (string, optional, nullable): Longest-matching routed CIDR prefix in the active Radix/LPM snapshot.
- `geo` (GeoLocation, optional): Country and city geolocation record (`geo_country` and `geo_city` families).
  - `country_code` (string, optional, nullable): Effective ISO 3166-1 alpha-2 country code (`"US"`, `"DE"`, `"TR"`).
  - `declared_country_code` (string, optional, nullable): Country code registered in RIR WHOIS/RDAP allocation records or RFC 8805 geofeed.
  - `physical_country_code` (string, optional, nullable): Country code where active BGP routing and latency triangulation place the infrastructure.
  - `is_virtual_location` (boolean, required): `true` when `declared_country_code` differs from `physical_country_code` (cross-border routed or geofeed-relocated prefix).
  - `location_type` (string, required): How the effective location was resolved by the lookup engine.
  - `confidence` (number, optional, nullable): Calibrated geolocation confidence score.
  - `subdivision` (string, optional, nullable): First-level administrative region or state. Included when `geo_city` is licensed.
  - `city` (string, optional, nullable): Resolved city name. Included when `geo_city` is licensed.
  - `accuracy_radius_km` (integer, optional, nullable): Estimated accuracy radius in kilometres around the city centroid. Included when `geo_city` is licensed.
  - `timezone` (string, optional, nullable): IANA time zone identifier. Included when `geo_city` is licensed.
- `network` (NetworkInfo, optional): BGP origin autonomous system metadata (`network` family).
  - `asn` (integer, optional, nullable): Origin Autonomous System Number without the `AS` prefix.
  - `organization` (string, optional, nullable): Canonical autonomous system or operator organization name.
- `connection` (ConnectionInfo, optional): Access network classification (`connection` and `hosting` families).
  - `connection_type` (string, required): Primary access network classification.
  - `is_residential` (boolean, required): `true` for consumer FTTH, cable, and DSL broadband ISPs.
  - `is_mobile` (boolean, required): `true` for cellular carrier gateways (4G/5G CGNAT pools).
  - `is_business` (boolean, required): `true` for enterprise leased lines and corporate autonomous systems.
  - `is_hosting` (boolean, required): `true` for cloud providers, colocation facilities, and VPS networks.
- `security` (SecurityInfo, optional): VPN, proxy, Tor, and hosting anonymizer classification (`anonymity` and `hosting` families).
  - `is_vpn` (boolean, required): `true` when the IP is an active commercial or self-hosted VPN exit node.
  - `is_hosting` (boolean, required): `true` when routed by a datacenter, cloud, or hosting ASN.
  - `is_tor_exit` (boolean, required): `true` when listed in the active Tor Project exit consensus.
  - `is_proxy` (boolean, required): `true` for open HTTP/SOCKS proxies and residential proxy relays.
  - `anonymizer_type` (string, required): Primary anonymizer classification.
  - `confidence` (number, optional, nullable): Calibrated anonymity confidence score.
  - `reason_codes` (string[], required): Deterministic evidence codes explaining why the IP was flagged.
- `security_extensions` (SecurityExtensions, optional): Extended threat telemetry and scanner recency (`security_extensions` family).
  - `abuse_velocity_score` (number, optional, nullable): Normalized abuse velocity score from active honeypot and darknet observations.
  - `is_known_scanner` (boolean, optional, nullable): `true` when the IP has been observed performing internet-wide port or vulnerability scanning.
  - `observation_recency_hours` (integer, optional, nullable): Hours elapsed since the most recent active security or anonymizer observation.
- `dataset_version` (string, required): Version identifier of the compiled IPScope dataset snapshot serving the response.
- `updated_at` (string, optional, nullable): ISO 8601 UTC timestamp when the active dataset snapshot was published.

```json 200 IpLookupResponse
{
  "ip": "198.51.100.7",
  "ip_version": 4,
  "matched_network": "198.51.100.0/24",
  "geo": {
    "country_code": "US",
    "declared_country_code": "US",
    "physical_country_code": "US",
    "is_virtual_location": false,
    "location_type": "physical",
    "confidence": 0.94,
    "subdivision": "Virginia",
    "city": "Ashburn",
    "postal_code": "20147",
    "latitude": 39.0438,
    "longitude": -77.4874,
    "accuracy_radius_km": 10,
    "timezone": "America/New_York"
  },
  "network": {
    "asn": 64501,
    "organization": "Example Cloud LLC"
  },
  "connection": {
    "connection_type": "hosting",
    "is_hosting": true,
    "is_residential": false,
    "is_mobile": false,
    "is_business": false
  },
  "security": {
    "is_vpn": true,
    "is_hosting": true,
    "is_tor_exit": false,
    "is_proxy": false,
    "anonymizer_type": "vpn",
    "confidence": 0.96,
    "reason_codes": [
      "vpn_handshake_confirmed",
      "datacenter_asn_range"
    ]
  },
  "security_extensions": {
    "abuse_velocity_score": 18,
    "is_known_scanner": false,
    "observation_recency_hours": 12,
    "anonymizer_reason": "vpn_handshake_confirmed",
    "threat_level": "medium",
    "abuse_velocity": "low",
    "confidence": 0.96
  },
  "dataset_version": "2026-10-27T00:00:00Z.crawl-1793088000000",
  "updated_at": "2026-10-27T00:00:00Z"
}
```

### 401 `ApiError`: Missing, malformed, expired, or revoked API key / Bearer token.

- `statusCode` (integer, required): HTTP status code returned by the API gateway.
- `message` (string, required): Human-readable diagnostic message describing the error condition.
- `error` (string, required): Canonical HTTP error classification.

```json 401 ApiError
{
  "statusCode": 401,
  "message": "Invalid or revoked API key",
  "error": "Unauthorized"
}
```

### 402 `ApiError`: Prepaid credit reservation exhausted (100 micro-USD per lookup).

- `statusCode` (integer, required): HTTP status code returned by the API gateway.
- `message` (string, required): Human-readable diagnostic message describing the error condition.
- `error` (string, required): Canonical HTTP error classification.

```json 402 ApiError
{
  "statusCode": 402,
  "message": "Prepaid credit reservation exhausted. Please top up your wallet.",
  "error": "Payment Required"
}
```

### 429 `ApiError`: Per-user token bucket rate limit exceeded (rl:user:<userId>).

- `statusCode` (integer, required): HTTP status code returned by the API gateway.
- `message` (string, required): Human-readable diagnostic message describing the error condition.
- `error` (string, required): Canonical HTTP error classification.

```json 429 ApiError
{
  "statusCode": 429,
  "message": "Rate limit exceeded (600 req/min). Retry after 14s.",
  "error": "Too Many Requests"
}
```

## Service endpoints

All endpoints are versioned under `/v1` and grouped into two controllers: **Lookup & Telemetry** (`LookupController`) and **MMDB & Artifact Distribution** (`MmdbController`).

### Lookup & Telemetry (`LookupController`)

| Method | Route | Scope / Auth | Metering | Description |
| --- | --- | --- | --- | --- |
| `GET` | [`/v1/ip/{ip}`](https://ipscope.io/docs/api-reference/endpoints/ip) | `lookup` | `1 credit` | Resolve any IPv4 or IPv6 address with live BGP/RIPEstat enrichment-on-miss. |
| `GET` | [`/v1/ip/me`](https://ipscope.io/docs/api-reference/endpoints/me) | Public (`None`) | Free (`120/min`) | Proxy-aware lookup of the connecting client’s public IP (alias: `/v1/public/ip`). |
| `POST` | [`/v1/ip/batch`](https://ipscope.io/docs/api-reference/endpoints/batch) | `batch` | `1 credit / IP` | Resolve 1 to 100 IPv4/IPv6 addresses pinned to a single consistent snapshot version. |
| `GET` | [`/v1/networks/{prefix}/{length}`](https://ipscope.io/docs/api-reference/endpoints/asn) | `lookup` | Included | Inspect canonical prefix record by split CIDR or URL-encoded `/v1/networks/{cidr}`. |
| `GET` | [`/v1/metadata/datasets`](https://ipscope.io/docs/api-reference/endpoints/meta) | `lookup` | Included | List active and previous snapshot versions, SHA-256 digests, and prefix counts. |
| `POST` | [`/v1/feedback`](https://ipscope.io/docs/api-reference/endpoints/meta) | `lookup` | Free | Submit a network operator or researcher geolocation/anonymizer correction report. |
| `GET` | [`/v1/status`](https://ipscope.io/docs/api-reference/endpoints/meta) | Public (`None`) | Free | Unauthenticated liveness probe returning active and previous snapshot versions. |

### MMDB & Artifact Distribution (`MmdbController`)

| Method | Route | Scope | Description |
| --- | --- | --- | --- |
| `GET` | [`/v1/mmdb/products`](https://ipscope.io/docs/api-reference/filtering-fields) | `lookup` | Catalog of supported formats (`mmdb`, `csv`, `parquet`), field families, cadences, and presets. |
| `POST` | [`/v1/mmdb/quote`](https://ipscope.io/docs/api-reference/filtering-fields) | `lookup` | Calculate a deterministic integer-minor-unit price quote and `profileSchemaHash`. |
| `GET` | [`/v1/mmdb/releases`](https://ipscope.io/docs/api-reference/filtering-fields) | `download` | List compiled release manifests, artifact sizes, and SHA-256 checksums. |
| `POST` | [`/v1/mmdb/releases/{id}/download-token`](https://ipscope.io/docs/api-reference/filtering-fields) | `download` | Issue a short-lived signed download token for a specific release (`id` or `latest`) and format. |
| `GET` | [`/v1/mmdb/releases/{id}/checksums`](https://ipscope.io/docs/api-reference/filtering-fields) | `download` | Fetch JSON or `text/plain` `SHA256SUMS` verification manifest. |
| `GET` | [`/v1/mmdb/releases/{id}/download`](https://ipscope.io/docs/api-reference/filtering-fields) | `download` | Stream `.mmdb`, `.csv.gz`, or `.parquet` with `ETag` (`304`), `Range` (`206`), or GCS V4 `302` redirect. |
| `GET` | [`/v1/mmdb/snapshots/diff`](https://ipscope.io/docs/api-reference/filtering-fields) | `download` | Compute a differential prefix update package between `from_version` and `to_version`. |

## Authentication & API key scopes

Authenticated endpoints are protected by `ApiKeyGuard`. Pass your API key in either the `Authorization: Bearer <key>` header or the `x-api-key: <key>` header. Keys are hashed with SHA-256 and verified in sub-millisecond memory against the hot key cache.

```bash Authorization: Bearer (recommended)
curl -sS "https://api.ipscope.io/v1/ip/198.51.100.7" \
  -H "Authorization: Bearer ips_live_••••••••"
```

```bash x-api-key header
curl -sS "https://api.ipscope.io/v1/ip/198.51.100.7" \
  -H "x-api-key: ips_live_••••••••"
```

Every API key is provisioned with explicit **permission scopes** (`ApiKeyScope`) and **licensed field families** (`licensedFamilies`). If a key lacks the required scope for a route, the API returns `403 Forbidden`.

| Scope | Granted endpoints | Purpose |
| --- | --- | --- |
| `lookup` | `/v1/ip/:ip`, `/v1/networks/*`, `/v1/metadata/datasets`, `/v1/feedback`, `/v1/mmdb/products`, `/v1/mmdb/quote` | Real-time single IP and CIDR lookups, dataset metadata, and feedback. |
| `batch` | `POST /v1/ip/batch` | High-throughput 100-IP batch enrichment requests. |
| `download` | `/v1/mmdb/releases*`, `/v1/mmdb/snapshots/diff` | Automated CI/CD and server cron jobs pulling `.mmdb`, `.csv.gz`, and `.parquet` snapshots. |
| `admin` | Administrative control plane | Internal platform and snapshot management. |

## Per-user rate limiting & headers

Rate limits are enforced **per user account** (`rl:user:<userId>`), shared across all active API keys owned by that user. Creating multiple keys for microservice isolation never multiplies burst limits or bypasses account-level rate governance.

| Response Header | Example Value | Description |
| --- | --- | --- |
| `X-RateLimit-Scope` | `user` | Indicates rate limiting is enforced at the user account boundary across all keys. |
| `X-RateLimit-Limit` | `600` | Maximum requests permitted per 60-second window for your account tier. |
| `X-RateLimit-Remaining` | `594` | Remaining requests allowed in the current 60-second window. |
| `X-RateLimit-Reset` | `38` | Seconds remaining until the current rate-limit window resets. |
| `Retry-After` | `14` | Sent on `429 Too Many Requests`; number of seconds to wait before retrying. |

## Prepaid credit reservation & metering

Metered endpoints (`GET /v1/ip/:ip` = `1` unit, `POST /v1/ip/batch` = `ips.length` units) debit from a low-latency Valkey credit reservation chunk backed by our PostgreSQL double-entry ledger (`100 micro-USD` per lookup = **$0.10 per 1,000 lookups**).

- **Atomic pre-validation**: On `POST /v1/ip/batch`, every IP in `ips[]` is validated *before* credits are debited. Malformed requests (`400 Bad Request`) never consume credits.
- **Explicit `402 Payment Required`**: When your monthly plan allowance and prepaid wallet reservation are exhausted, metered endpoints return `402 Payment Required` rather than silently accruing surprise post-paid overage bills.
- **Unmetered operational endpoints**: `GET /v1/status`, `GET /v1/ip/me`, `GET /v1/metadata/datasets`, and `POST /v1/feedback` do not debit lookup credits.

## Explore endpoint specifications

- [GET /v1/ip/{ip}](https://ipscope.io/docs/api-reference/endpoints/ip) (scope: lookup): Single IPv4/IPv6 lookup with live BGP/RIPEstat enrichment-on-miss and licensed family projection.
- [GET /v1/ip/me](https://ipscope.io/docs/api-reference/endpoints/me) (Public): Keyless visitor IP lookup with TRUST_PROXY Cloudflare edge header resolution (120 req/min).
- [POST /v1/ip/batch](https://ipscope.io/docs/api-reference/endpoints/batch) (scope: batch): Resolve up to 100 IPv4/IPv6 addresses in a single request pinned to one snapshot version.
- [GET /v1/networks/{prefix}/{length}](https://ipscope.io/docs/api-reference/endpoints/asn) (scope: lookup): Inspect canonical network prefix records by split CIDR or URL-encoded CIDR.
- [Datasets, Feedback & Status](https://ipscope.io/docs/api-reference/endpoints/meta) (Metadata): GET /v1/metadata/datasets, POST /v1/feedback, and GET /v1/status health checks.
- [Field Families & MMDB API](https://ipscope.io/docs/api-reference/filtering-fields) (scope: download): Licensed field family projection and /v1/mmdb/* release, quote, checksum, and download endpoints.
