IP intelligence

One lookup places an IPv4 or IPv6 address on the map, names the network and the operator, and flags the traffic that is not a person on a home connection: cloud and datacenter ranges, Tor exits, named VPN exits, iCloud Private Relay, and addresses on public threat blocklists, and names the verified search and AI crawlers. Every signal comes from a local database or a compiled feed, so an answer takes milliseconds and the address never leaves the server unless you ask for the registry’s abuse contact.

$ curl https://spaw.co/api/v1/ip \
    -H "Authorization: Bearer sk_live_…" \
    -H "Content-Type: application/json" \
    -d '{"ip": "8.8.8.8"}'

Fields and flags

Every response carries the same fields in the same order. The threat flags are tri-state: false is a checked negative and null means the signal was not evaluated, because no feed for it covers the address family. Every feed group, Tor included, carries IPv6 today.

ipThe canonical form of the address; IPv4-mapped IPv6 input collapses to dotted-quad.
versionThe address family.
reasonreserved_range for private, loopback, CGNAT, documentation and multicast space. Every other field is then null and the call is free.
networkThe CIDR prefix the location answer covers, such as 8.8.8.0/24.
continentTwo-letter continent code: AF, AN, AS, EU, NA, OC, SA.
countryISO 3166-1 alpha-2 code.
country_nameThe English name of the country.
is_euWhether the country is a member of the European Union. Null when the country is unknown.
is_eeaWhether the country is in the European Economic Area: the EU plus Iceland, Liechtenstein and Norway, where the GDPR applies directly. Null when the country is unknown.
privacy_regimeThe comprehensive privacy law where the address is, as a lowercase code: the region first where a state or province has its own law (California ccpa, Quebec law25), otherwise the country (gdpr across the EU and EEA, uk_gdpr, fadp, lgpd, pipeda, appi, pipl, popia, …). Null means no comprehensive law is known for the place, never that there is none; read the code with the country, since pdpa names different laws in Singapore, Thailand, Malaysia and Taiwan.
regionState or province name. Null when an operator-declared location disagrees with the database.
cityCity name. Approximate from the database; exact where the operator publishes it.
latitude / longitudeCoordinates of the located area. Null when an operator-declared location disagrees with the database, rather than a contradicting point.
location_sourceoperator when the network operator publishes the location itself (an RFC 8805 geofeed, Apple’s relay list, a VPN provider’s server list); database for the DB-IP Lite answer.
location_confidencehigh when the operator declared the location itself; otherwise graded from the weekly accuracy report’s city agreement for the country: 75 % and above high, 50 % medium, below that low. Null with fewer than 20 compared samples for the country or no report yet.
timezoneIANA zone such as Europe/Berlin, only when the country or a region known to lie in one zone settles it. Split regions such as Texas answer null.
currencyISO 4217 currency of the country, from ICU region data.
calling_codeThe country’s international calling code, such as 49.
asn / orgThe autonomous system number and the organisation operating it.
asn_typehosting, isp, education or government. hosting whenever is_datacenter is true or the operator name says so; null when it cannot be told.
registryThe regional registry that allocated the space: arin, ripencc, apnic, lacnic or afrinic, from the RIRs’ daily delegated statistics.
registry_countryThe country the registry recorded for the allocation. When it disagrees with the geolocation, look twice.
allocated_atThe allocation date the registry recorded (YYYY-MM-DD). A block allocated or transferred weeks ago is a classic fraud tell; a fact for you to weigh, not scored.
abuse_contactThe network’s abuse mailbox from its RDAP record, only when asked for with abuse_contact: true (a registry lookup, cached per allocation). Null otherwise or when the registry lists none.
network_detailsOnly with network_details: true: the RDAP record’s name, organisation, range, registered_at and updated_at, from the same cached registry lookup as abuse_contact. Null otherwise or when the registry could not be read.
hostnameOnly with hostname: true: the reverse DNS (PTR) name of the address, a live DNS query with a two-second budget. Null otherwise or when there is none.
hostname_verifiedWhether the PTR name resolves back to the address (forward-confirmed reverse DNS). A verified name under googlebot.com, google.com, search.msn.com, applebot.apple.com, yandex.* or crawl.baidu.* also proves the crawler. Null unless hostname was requested.
is_datacenterInside known hosting or cloud ranges: the providers’ own feeds plus the open X4BNet list.
hosting_providerAmazon Web Services, Google Cloud, Microsoft Azure, Oracle Cloud, DigitalOcean, Linode, Vultr, Cloudflare or Fastly when the address is inside that provider’s own published ranges.
is_crawlerInside the ranges a crawler operator publishes for verification (Googlebot and Google’s special crawlers and fetchers, Bingbot, GPTBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, Perplexity-User, DuckDuckBot, AhrefsBot), or proved by a verified hostname. Not a risk: no weight, and the datacenter weight is skipped for it.
crawlerThe crawler that owns the range or the verified name: Googlebot, Bingbot, GPTBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, Perplexity-User, DuckDuckBot, AhrefsBot, Applebot, YandexBot, Baiduspider, Google special crawlers or Google user-triggered fetchers. Null unless is_crawler is true.
crawler_kindWhat the crawler does: search, ai_training, ai_assistant, seo or fetcher (Google’s special crawlers and user-triggered fetchers).
is_anycastA published anycast range: CDN edges and the public resolvers. Geolocation means little for such an address.
is_unallocatedNo regional Internet registry has allocated or assigned the space (the five RIRs’ daily statistics). Such an address should not be on the public Internet; it scores 100.
is_blocklistedA public threat blocklist names the address’s netblock or its operator: Spamhaus DROP (netblocks hijacked or leased by spam and cybercrime operations), Spamhaus ASN-DROP (the operators behind them, matched by ASN) and abuse.ch’s Feodo Tracker list of active botnet C2 servers. Scores 80.
blocklistThe list that names the address: Spamhaus DROP, Spamhaus ASN-DROP or abuse.ch Feodo Tracker, the first in that order when several do.
is_torA listed Tor exit, from the Tor Project’s bulk exit list and its Onionoo relay data, so IPv6 exits are covered too.
is_vpnA known VPN exit: Mullvad, NordVPN and Private Internet Access server lists plus the open X4BNet list. False on iCloud Private Relay egress unless a named list has the address.
is_relayAn iCloud Private Relay egress address, from Apple’s published list.
privacy_serviceMullvad, NordVPN, Private Internet Access or iCloud Private Relay when a provider’s own list names the address.
is_anonymousTrue when any of is_tor, is_vpn or is_relay is true; false when at least one was checked and none matched; null when none was evaluated.
risk_scoreUnallocated 100 + blocklist 80 + Tor 70 + reported_abuse 60 + datacenter 40 + reported_abuse_widely 40 + VPN 30 + relay 10 over the signals checked, capped at 100. Null only when no feed evaluated the address and nobody reported it.
risk_levelhigh from 60, medium from 30, low below that.
risk_signalsEach contributing signal with the weight applied and the dataset behind it; the weights sum to risk_score.
sourcesThe datasets consulted, each with the date it was last synced. Only groups that were evaluated appear.

Where the network operator publishes the location itself, that declaration wins over the database and location_source says operator: RFC 8805 geofeeds from DigitalOcean, Linode and Vultr, Apple’s relay egress list, and the server lists of Mullvad and NordVPN name the city, Private Internet Access the country. When the operator disagrees with the database on country or city, the region and the coordinates answer null instead of a point in the wrong place.

Country facts come from data the service already ships: currency from ICU region data, calling_code from libphonenumber, and timezone from the IANA zone list only when the country or a region known to lie in a single zone settles it. A visitor in California gets America/Los_Angeles; a visitor in Texas, which straddles two zones, gets null rather than a dominant guess.

The threat blocklists are Spamhaus DROP and ASN-DROP, published by The Spamhaus Project and used with the credit its terms ask for, and abuse.ch’s Feodo Tracker (CC0). A hit names the list in blocklist. ASN-DROP can only answer once the ASN database resolves the address, so when neither a range list nor the ASN could settle it the flag answers null rather than false.

The crawler ranges are the files Google, Microsoft, OpenAI, Perplexity, DuckDuckGo and Ahrefs publish so sites can verify their bots, synced daily. City answers improve over time from the RFC 8805 geofeeds discovered across the five registries every week, and the registry facts come from the same daily delegated statistics that answer is_unallocated.

Risk score

The score is arithmetic over the flags in the same response, capped at 100. Recompute it, or ignore it and branch on the booleans. Two rules apply: the datacenter weight is skipped when is_relay or is_crawler is true, because relay egress and verified crawlers sit on infrastructure by design, and is_vpn answers false on relay egress unless a VPN provider’s own list names the address, because Apple’s first-party declaration outranks the aggregate list that also carries those ranges.

Unallocated space (no registry has assigned it)+100
Threat blocklist (Spamhaus DROP or ASN-DROP, abuse.ch Feodo Tracker)+80
Tor exit+70
Reported by you as abuse, fraud, a bot or a chargeback inside the window+60
datacenter or cloud range+40
Reported by three or more accounts+40
VPN exit+30
iCloud Private Relay egress+10

Worked examples, as the API answers them:

Home broadband, nothing matchednone0, low
Google’s public resolver 8.8.8.8, in the aggregate datacenter listdatacenter 4040, medium
A Mullvad exit hosted in a datacenterdatacenter 40, VPN 3070, high
A Tor exit on a hosting providerTor 70, datacenter 40100, high (capped)
A netblock on Spamhaus DROP, nothing else matchedblocklist 8080, high
iCloud Private Relay egress on a CDNdatacenter 0 (skipped for relay), relay 1010, low
Googlebot on Google’s own published rangesdatacenter 0 (skipped for a crawler); is_crawler true0, low
Home broadband you reported as fraud last weekreported_abuse 6060, high
IPv6 address, Tor list not evaluated, nothing else matchednone; is_tor null0, low
Private address 192.168.1.1reserved_rangenull, free

risk_signals spells the sum out so nobody re-implements it. For the relay example above it reads:

"risk_signals": [
  { "signal": "datacenter", "weight": 0,  "dataset": "datacenter-ranges" },
  { "signal": "relay",      "weight": 10, "dataset": "relay-ranges" }
],
"risk_score": 10,
"risk_level": "low"

Residential proxies are not detectable with open data and are not claimed. When that signal ships it will be a separately labelled field, never folded into these weights.

Self and browser lookups

The same lookup is available three ways from a server. POST /api/v1/ip takes the address in the body, GET /api/v1/ip/{ip} takes it in the path, and GET /api/v1/ip/me answers for the address the call came from, as seen through the proxy in front of the service. Options ride in the body or the query string.

$ curl https://spaw.co/api/v1/ip/me \
    -H "Authorization: Bearer sk_live_…"

$ curl "https://spaw.co/api/v1/ip/2001:4860:4860::8888?privacy=1" \
    -H "Authorization: Bearer sk_live_…"

From a page, use a publishable key against POST /api/v1/ip/public. Create the key on the API keys page and list the domains it may run from; the endpoint checks the browser’s Origin header against that list. Leave ip out and the visitor’s own address is looked up, which is the usual case for choosing a language, a currency, or a consent banner on first paint:

const response = await fetch('https://spaw.co/api/v1/ip/public', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'pk_live_…' }),
});
const { data } = await response.json();

if (data.is_eu) showConsentBanner();
if (data.currency) setDefaultCurrency(data.currency);
if (data.is_anonymous) requireEmailConfirmation();

Lookups bill the key’s owner and the response carries no meta block, so visitors never see the balance. Because the key sits in page source, give it a daily credit cap; once spent, the endpoint answers 429 KEY_SPEND_CAP_REACHED until the next day, and free answers never count toward it. A key that carries a Cloudflare Turnstile pair requires a confirmed turnstile_token with every lookup. The endpoint is throttled at 20 requests a minute per visitor.

Batch and bulk

POST /api/v1/ip/batch looks up as many as 1,000 addresses in one call, in input order, under the single-lookup billing rules. Repeats inside the list are looked up once. If the balance runs out mid-list, the paid partial results come back with meta.stopped_reason: "insufficient_credits"; only a batch whose first lookup is refused answers the typed 402. A batch counts as one request against the rate limit.

$ curl https://spaw.co/api/v1/ip/batch \
    -H "Authorization: Bearer sk_live_…" \
    -H "Content-Type: application/json" \
    -d '{"ips": ["8.8.8.8", "103.124.165.2", "192.168.1.1"]}'

For log files and exports, a bulk run queues up to 100,000 addresses with POST /api/v1/ip/bulk or from the IP intelligence page in the dashboard. Poll GET /api/v1/ip/bulk/{jobId} for progress and the running low, medium and high counts, then download the CSV from GET /api/v1/ip/bulk/{jobId}/results: one row per input address, in input order, with these columns:

ip, version, reason, network, continent, country, country_name, is_eu, is_eea, privacy_regime, region, city, latitude, longitude, location_source, location_confidence, timezone, currency, calling_code, asn, org, asn_type, registry, registry_country, allocated_at, is_datacenter, hosting_provider, is_crawler, crawler, crawler_kind, is_blocklisted, blocklist, is_tor, is_vpn, is_relay, privacy_service, is_anonymous, risk_score, risk_level, credits_used

Cancelling stops a queued job on the spot and a running one at its next checkpoint; rows already answered stay billed and downloadable. With a webhook_url, the finish is POSTed there as bulk_ip_job.finished, signed in X-Spaw-Signature with the HMAC-SHA256 of the raw body keyed with the secret creation returns once. Send an Idempotency-Key header to make creation safe to retry: the same key with the same request replays the job with Idempotent-Replayed: true, the same key with a different request answers 409 IDEMPOTENCY_KEY_REUSED.

IP endpoints have their own rate limit of 50 requests per second per key, separate from the 5 per second that applies to the email endpoints.

Outcome feedback

A lookup can only say what the feeds know. What you learn afterwards, a confirmed fraudulent order, a scraper, a bot that got through, a chargeback, can be sent back so your own later lookups of that address carry it: POST /api/v1/ip/feedback takes up to 1,000 items of an address and an outcome, abuse, fraud, bot or chargeback, with an optional occurred_at. It is free and never logged as a lookup.

$ curl https://spaw.co/api/v1/ip/feedback \
    -H "Authorization: Bearer sk_live_…" \
    -H "Content-Type: application/json" \
    -d '{"items": [{"ip": "203.0.113.9", "outcome": "fraud"}]}'

Any report you made about an address inside the last 30 days adds reported_abuse (+60, dataset abuse-feedback) to your own lookups of it. Once three or more accounts report the same address inside 30 days, everyone's lookups of it carry reported_abuse_widely (+40); only the number of reporting accounts is ever read, never who reported or what. Neither signal turns a null score into a number: an address no feed evaluated stays unscored.

Reports are the one thing the IP product stores on purpose, and even they never hold the address: each is a salted SHA-256 hash of the canonical address with the outcome and a timestamp, which can confirm a later lookup of the same address but cannot be read back into one. Malformed and reserved addresses are skipped and counted, not rejected. Reports are kept for 90 days, and GET /api/v1/ip/feedback/summary answers that window: totals per outcome, distinct addresses, and the IPv4 and IPv6 split.

Monitored lists

Your own addresses deserve the same check as your visitors’. A monitored list ( POST /api/v1/ip/monitors) keeps up to 500 of them, egress addresses, mail servers and office connections, under a daily, weekly or monthly re-check. The first run sets the baseline; from the next run on, every address that newly lands on a threat blocklist, becomes a Tor exit or scores high arrives by email with the reason, so you learn that a mail server sits on Spamhaus DROP before your recipients’ inboxes do.

Each run bills every address like a single lookup, and the seven-day repeat marker applies, so a daily list costs at most one credit per address a week; reserved addresses are free. GET /api/v1/ip/monitors/{id} answers the last run per address: its status (clean, flagged or reserved), the flags that fired (blocklisted, tor, high_risk), the list that names it, and whether it changed. POST /api/v1/ip/monitors/{id}/run re-checks the list ahead of schedule, and DELETE stops it.

Privacy

An IP address is personal data, and the service treats it that way. Nothing about an IP lookup is stored: no history row, no log line with the address. The only trace is a keyed seven-day marker that makes a repeat of the same address free. Send privacy: true in the body, or ?privacy=1 on a GET, to skip even that marker; every such call is then a fresh, billable lookup.

IP lookups never leave the server. Every signal is a local database or a compiled feed, so no third-party processor sees the address, with one opt-in exception: asking for abuse_contact sends the address to the Internet registry’s public RDAP service (via rdap.org) to read the network’s abuse mailbox, cached per allocation. The sources block names each dataset consulted and the date it was synced. Bulk input lists and result files are the only copies of what you sent, and both are deleted with the job after 30 days.

The security page states the same facts alongside the rest of the data handling, and a data processing agreement is available on request from [email protected].

Next: the endpoint reference for every field, example, and error code.