Skip to content

API reference/ip

Locate and risk-score an IP address

POST/api/v1/ip

authentication
Secret API key as a bearer token
billing
1 credit per 5 fresh lookups that resolve at least one signal (a location, a network, or a positive threat flag: Tor, datacenter, VPN, relay, blocklist, crawler or corporate proxy): the answered lookup that opens a block costs 1 credit and the next four cost nothing, with `meta.ip_lookups_prepaid` counting what is left. Reserved and private ranges, unallocated space, unknown addresses and 7-day repeats are free and use none of the block. Privacy-mode lookups are always fresh.

Locates an IPv4 or IPv6 address with the DB-IP Lite city and ASN databases, applies the network operator's own published location where a geofeed or provider list carries one (location_source: "operator"), and flags datacenter, Tor exit, VPN, iCloud Private Relay and corporate-gateway addresses from open and first-party feeds: the Tor Project's exit list and Onionoo relay data, X4BNet's datacenter and VPN lists, the published ranges of AWS, Google Cloud, Microsoft Azure, Oracle Cloud, DigitalOcean, Linode, Vultr, Cloudflare, Fastly and GitHub's Actions runners, the server lists of Mullvad, NordVPN, Private Internet Access, IVPN, AirVPN and the VPN Gate volunteer relays, Apple's relay egress list, and the egress ranges Zscaler publishes for its clouds. hosting_provider, privacy_service and corporate_proxy name the provider when its own feed contains the address. Cloudflare publishes no list of its WARP egress, so WARP is inferred as Cloudflare's own network outside the CDN ranges it does publish, and privacy_service then reads Cloudflare WARP (inferred). is_anonymous is true when any of Tor, VPN or relay is. is_crawler flags the ranges the search, AI and SEO crawlers publish for verification (Googlebot and Google's fetchers, Bingbot, GPTBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, Perplexity-User, DuckDuckBot, AhrefsBot), with crawler and crawler_kind naming them; a crawler is not a risk, so it carries no weight and, like relay egress and a corporate gateway, never the datacenter weight.

Country-level facts come with the location: country_name, continent, is_eu, is_eea, privacy_regime (the comprehensive privacy law where the address is, the region first: ccpa in California, gdpr across the EU and EEA, uk_gdpr, lgpd, …, null when none is known), sanctions (the programmes naming the country: ofac_comprehensive for Cuba, Iran and North Korea, ofac_programme for any country-related OFAC programme; an empty list when none does, null when the country is unknown) and fatf_list (call_for_action or increased_monitoring, as of the FATF's June 2026 plenary) — facts about a place, never a verdict about a person — plus currency, calling_code, and timezone with timezone_source: the curated map settles the zone by region or country where a place keeps one clock, and where it cannot (Texas, most of Canada, Australia, Brazil, Russia) the nearest GeoNames place inside the located country within 250 km does (nearest_city); null when neither can. Send browser_timezone, the zone the browser reported to your page, and timezone_matches says whether it keeps the same clock as the located zone. network is the prefix the location answer covers, and location_confidence grades the city: low for an anycast, mobile-carrier or satellite address whoever placed it, else high when the operator declared it, otherwise from the measured agreement for that country in the weekly accuracy report. registry, registry_country and allocated_at come from the RIR delegated statistics; a freshly allocated block, or a registry country that disagrees with the geolocation, is worth a second look. connection_type says what the address is attached to from positive evidence only — corporate_proxy, hosting, satellite, mobile, or residential from a dynamic reverse-DNS name — and null never means residential; is_mobile and is_satellite come from curated, positive-only operator lists, so false means "not listed".

Two more feeds answer questions the others cannot: is_unallocated is true when no regional Internet registry has allocated or assigned the space, from the five RIRs' daily statistics, which is the strongest sign of a spoofed or forged address; is_anycast marks published anycast ranges such as CDN edges and the public resolvers. is_blocklisted is true when a public threat blocklist names the address's netblock or its operator: Spamhaus DROP, Spamhaus ASN-DROP (matched by the address's ASN) and abuse.ch's Feodo Tracker list of active botnet C2 servers, with blocklist naming the list. is_attack_source is true when an attack-source list names the address — single hosts seen attacking honeypots and sensors in the last days: Emerging Threats' compromised-hosts list, the CINS Army list and blocklist.de — with attack_source naming the list; these name hosts rather than netblocks and churn daily, so they sit apart from the blocklists, weigh 50 and are refreshed hourly. Tor exits are read from both the Tor Project's bulk list and its Onionoo relay data, refreshed hourly, so is_tor is evaluated for IPv6 too; a thirty-day exit history answers tor_last_seen, the date the address was last seen exiting, and an address that left the list within the last week still carries the recently_tor weight. Send abuse_contact: true to add the network's abuse mailbox from the registry's RDAP record; it is a network lookup, cached per allocation, so it is only made when asked for. network_details: true adds the record's name, organisation, range and dates from the same cached lookup, and hostname: true adds the reverse DNS name with hostname_verified saying whether it forward-confirms, hostname_kind reading the naming convention as dynamic or static, and hostname_domain its registrable domain; a verified name under a crawler operator's domain also proves the crawler. The RFC 8805 geofeeds discovered across the registries every week feed the location override too.

Your account's own allow and deny lists (/api/v1/ip/lists) apply to every lookup the account is billed for: account_list names the list and the entry that matched, an allow-list hit answers risk_score: 0 with a single allowlisted signal of weight 0, and a deny-list hit adds denylisted 100. seen_by_you counts how often this account looked the address up in the seven days before this call and when it first did, under a keyed hash of the address; it is null under privacy mode and for reserved addresses.

The flags are tri-state: false is a checked negative, null means the signal was not evaluated (feed not installed, or no feed for that signal covers the address family). Apple's relay egress also appears in the aggregate VPN list, so when Apple's own list says relay and no VPN provider's list names the address, is_vpn answers false: the first-party declaration outranks the aggregate classification. risk_score sums only the signals that were checked — denylisted 100, unallocated 100, blocklist 80, Tor 70, reported_abuse 60, attack_source 50, datacenter 40, reported_abuse_widely 40, recently_tor 40, VPN 30, relay 10, capped at 100; the datacenter weight is skipped when is_relay, is_crawler or is_corporate_proxy is true because relay egress, verified crawlers and corporate gateways sit on infrastructure by design; reported_abuse is your own outcome reports for the address inside the window and reported_abuse_widely means three or more accounts reported it (see the feedback endpoint) — risk_signals lists each contributing signal with the weight applied, and risk_level buckets the score with the same thresholds as email: 60 and above high, 30 and above medium. Reserved and private ranges answer reason: "reserved_range" with every field null.

Nothing about an IP lookup is stored: no history row, no log line with the address. The only traces are a keyed seven-day marker that makes a repeat free and the keyed counter behind seen_by_you. Send privacy: true to skip both; every such call is then a fresh, billable lookup. Send fields, a comma-separated list of field names, to have data carry only those: unknown names are ignored, an empty list means everything, and sources is included only if named.

The same lookup is available as GET /api/v1/ip/{ip}, and GET /api/v1/ip/me answers for the address the call came from; both answer with Cache-Control: private, max-age=300. IP endpoints have their own rate limit of 50 requests per second per key.

Request body

field type description
iprequiredstringAn IPv4 or IPv6 address.
privacyboolean | nullKeep nothing about this lookup, not even the seven-day repeat marker. The call is billed as a fresh lookup.
abuse_contactboolean | nullAlso look up the network's abuse mailbox in the registry's RDAP record.
network_detailsboolean | nullAlso read the allocation's RDAP record for the network name, organisation, covered range and registration dates (one registry call, shared with abuse_contact and cached per allocation).
hostnameboolean | nullAlso resolve the reverse DNS name and forward-confirm it; a verified name from a crawler operator's domain sets is_crawler. Two DNS queries, cached six hours per address.
browser_timezonestring | nullThe IANA zone the browser reported to your page (`Intl.DateTimeFormat().resolvedOptions().timeZone`), compared with the located zone for `timezone_matches`. A zone the server does not know answers 422. At most 64 characters.
fieldsstring | nullA comma-separated list of the field names to answer with; `data` then carries only those, in the answer's own order. Unknown names are ignored, an empty list means everything, and `sources` is included only if named. `meta` is never narrowed. At most 1,000 characters.

Example request

curl -X POST https://spaw.co/api/v1/ip \
  -H "Authorization: Bearer sk_sandbox_spaw" \
  -H "Content-Type: application/json" \
  -d '{
  "ip": "8.8.8.8"
}'
const response = await fetch('https://spaw.co/api/v1/ip', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_sandbox_spaw',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "ip": "8.8.8.8"
  }),
});
const result = await response.json();
import requests

response = requests.post(
    'https://spaw.co/api/v1/ip',
    headers={'Authorization': 'Bearer sk_sandbox_spaw'},
    json={
        'ip': '8.8.8.8'
    },
)
result = response.json()
$ch = curl_init('https://spaw.co/api/v1/ip');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_sandbox_spaw', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'ip' => '8.8.8.8'
    ]),
]);
$result = json_decode(curl_exec($ch), true);
from spaw import Client

client = Client('sk_sandbox_spaw')
result = client.lookup_ip('8.8.8.8')
import Spaw from 'spaw';

const spaw = new Spaw({ apiKey: 'sk_sandbox_spaw' });
const result = await spaw.lookupIp('8.8.8.8');
use Spaw\Client;

$spaw = new Client('sk_sandbox_spaw');
$result = $spaw->lookupIp('8.8.8.8');

Run it

free demo · no key needed

Running a request from this page needs JavaScript. The command above makes the same call from a terminal.

Responses

200The location, network, flags and score.

{
    "success": true,
    "data": {
        "ip": "8.8.8.8",
        "version": 4,
        "reason": null,
        "network": "8.8.8.0/24",
        "continent": "NA",
        "country": "US",
        "country_name": "United States",
        "is_eu": false,
        "is_eea": false,
        "privacy_regime": "ccpa",
        "sanctions": [],
        "fatf_list": null,
        "region": "California",
        "city": "Mountain View",
        "latitude": 37.4056,
        "longitude": -122.0775,
        "location_source": "database",
        "location_confidence": "low",
        "timezone": "America/Los_Angeles",
        "timezone_source": "region",
        "timezone_matches": null,
        "currency": "USD",
        "calling_code": 1,
        "asn": 15169,
        "org": "Google LLC",
        "asn_type": "hosting",
        "connection_type": "hosting",
        "is_mobile": false,
        "is_satellite": false,
        "registry": "arin",
        "registry_country": "US",
        "allocated_at": "2023-12-28",
        "abuse_contact": null,
        "network_details": null,
        "hostname": null,
        "hostname_verified": null,
        "hostname_kind": null,
        "hostname_domain": null,
        "is_datacenter": true,
        "hosting_provider": null,
        "is_corporate_proxy": false,
        "corporate_proxy": null,
        "is_crawler": false,
        "crawler": null,
        "crawler_kind": null,
        "is_anycast": true,
        "is_unallocated": false,
        "is_blocklisted": false,
        "blocklist": null,
        "is_attack_source": false,
        "attack_source": null,
        "is_tor": false,
        "tor_last_seen": null,
        "is_vpn": false,
        "is_relay": false,
        "privacy_service": null,
        "is_anonymous": false,
        "account_list": null,
        "risk_score": 40,
        "risk_level": "medium",
        "risk_signals": [
            {
                "signal": "datacenter",
                "weight": 40,
                "dataset": "datacenter-ranges"
            }
        ],
        "seen_by_you": {
            "count": 0,
            "first_seen_at": null,
            "window_days": 7
        },
        "sources": [
            {
                "dataset": "dbip-city-lite",
                "version": "2026-09"
            },
            {
                "dataset": "dbip-asn-lite",
                "version": "2026-09"
            },
            {
                "dataset": "tor-exit-list",
                "version": "2026-09-12"
            },
            {
                "dataset": "datacenter-ranges",
                "version": "2026-09-12"
            },
            {
                "dataset": "vpn-ranges",
                "version": "2026-09-12"
            },
            {
                "dataset": "relay-ranges",
                "version": "2026-09-12"
            },
            {
                "dataset": "anycast-ranges",
                "version": "2026-09-12"
            },
            {
                "dataset": "rir-allocations",
                "version": "2026-09-12"
            },
            {
                "dataset": "threat-blocklists",
                "version": "2026-09-12"
            },
            {
                "dataset": "attack-source-lists",
                "version": "2026-09-12"
            },
            {
                "dataset": "crawler-ranges",
                "version": "2026-09-12"
            },
            {
                "dataset": "corporate-proxy-ranges",
                "version": "2026-09-12"
            },
            {
                "dataset": "satellite-asns",
                "version": "2026-09-12"
            },
            {
                "dataset": "mobile-carrier-asns",
                "version": "2026-09-12"
            },
            {
                "dataset": "tor-exit-history",
                "version": "2026-09-12"
            },
            {
                "dataset": "operator-geofeeds",
                "version": "2026-09-06"
            },
            {
                "dataset": "compliance-lists",
                "version": "2026-09-12"
            }
        ]
    },
    "meta": {
        "credits_used": 1,
        "credits_remaining": 8,
        "cache_hit": false,
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c",
        "ip_lookups_prepaid": 4
    }
}

401The key is missing, malformed, or revoked.

{
    "success": false,
    "error": {
        "code": "UNAUTHENTICATED",
        "message": "Provide a valid API key as a bearer token.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

402The balance is empty. The lookup did not run.

{
    "success": false,
    "error": {
        "code": "INSUFFICIENT_CREDITS",
        "message": "Your credit balance is empty.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

422The request body could not be validated; `error.errors` lists the fields.

{
    "success": false,
    "error": {
        "code": "VALIDATION_FAILED",
        "message": "The email field is required.",
        "errors": {
            "email": [
                "The email field is required."
            ]
        },
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

429Over 5 requests per second for the key. Retry after the limit resets.

{
    "success": false,
    "error": {
        "code": "RATE_LIMITED",
        "message": "Too many requests. Retry after the limit resets.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

Error codes

Failures answer { success: false, error: { code, message, request_id } }. Each code has its own page.

markdown version·openapi.json