# Single IP lookup

> Resolve a single IPv4 or IPv6 address against the active Radix/LPM snapshot with live BGP/RIPEstat enrichment-on-miss.

```http
GET https://api.ipscope.io/v1/ip/{ip}
```

Authentication: API key (`X-API-Key` header or `Authorization: Bearer`). Plan: scope: lookup · 1 credit

Resolves a single IPv4 or IPv6 address (`GET /v1/ip/:ip`). The address is parsed and normalized via `tryParseIp(ip)` before querying the in-memory `SnapshotLoaderService` Radix/LPM tree. Both full and RFC 5952 compressed IPv6 notations (`2606:4700:4700::1111`) are supported.

**Path parameters**

- `ip` (string, required): The IPv4 (`198.51.100.7`) or IPv6 (`2606:4700:4700::1111`) address to inspect. Returns `400 Bad Request` if malformed.

**Request headers & metering**

- `Authorization` (string, required): `Bearer <api_key>` or `x-api-key: <api_key>`. Must include the `lookup` scope.
- `Credit debit` (1 unit): Debits `1` lookup unit (`100 micro-USD` when using prepaid wallet reservation).

## Code examples

```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;
```

## Responses

### 200 `IpLookupResponse`: Resolved IPv4/IPv6 intelligence record projected to your API 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"
}
```

### 400 `ApiError`: Malformed IPv4 or IPv6 address in the {ip} path parameter.

- `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 400 ApiError
{
  "statusCode": 400,
  "message": "Invalid IPv4 or IPv6 address: \"999.0.0.1\"",
  "error": "Bad Request"
}
```

### 401 `ApiError`: Missing, malformed, or revoked API key.

- `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": "Missing or invalid 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"
}
```

### 403 `ApiError`: API key does not have the required lookup scope.

- `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 403 ApiError
{
  "statusCode": 403,
  "message": "API key does not have required scope: lookup",
  "error": "Forbidden"
}
```

### 429 `ApiError`: Per-user 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"
}
```

### 503 `ApiError`: No active dataset snapshot loaded on the API node.

- `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 503 ApiError
{
  "statusCode": 503,
  "message": "No active dataset snapshot loaded",
  "error": "Service Unavailable"
}
```

## Live BGP & RIPEstat enrichment-on-miss

When an IPv4 or IPv6 address does not match a pre-compiled city or ASN record in the active snapshot (or matches a prefix missing ASN/geolocation attributes) and `LIVE_ENRICH_ON_MISS=true` is enabled on the API node:

- The lookup engine queries live **RIPEstat / BGP origin ASN**, **RDAP remarks (`#geofeed`)**, and **active tracepath hints** in real time.
- The enriched record is merged into the response (`mergeLiveEnrichment`) and cached in the in-memory hot enrichment cache so subsequent lookups for the same IP or `/24` (`/48` IPv6) resolve instantaneously.
