API Documentation
One simple, fast REST API for IP geolocation, fraud scoring, WHOIS, DNS propagation, hostname resolution and email verification — all processed in-house. No data is sent to third parties, except that WHOIS and DNS propagation lookups necessarily query the registries and public resolvers you ask about.
curl "https://ipaddress.to/api/lookup/8.8.8.8"
Overview
All endpoints are plain GET requests returning JSON over HTTPS — no authentication, tokens or signup. Every response includes a success boolean; most include duration_ms (server processing time) and a cached flag when served from the short-lived server-side cache. All lookups run against our own local geolocation, ASN, VPN-provider and threat-intelligence databases, so your queries never leave our infrastructure.
/api/dns streams NDJSON — one JSON object per line.Access-Control-Allow-Origin: * — call it straight from the browser.Accounts & API Keys
Every endpoint works without any credentials — keys are optional. Creating a free account at /account/ gives you API keys (format ipa_<40 hex chars>; your first key is generated on signup, up to 5 active keys per account) so you can label integrations and see per-key request counts and last-used times on your account page. Keyed requests also make it possible for us to offer you higher limits than anonymous fair-use traffic if quotas are ever introduced.
Pass the key with any request, either as a header or a query parameter:
X-API-Key: ipa_your_key_herehttps://ipaddress.to/api/lookup/8.8.8.8?key=ipa_your_key_herePrefer the header in server-side code — query strings tend to end up in logs. An invalid or revoked key never causes an error; the request is simply treated as anonymous.
IP Lookup
Geolocation and network intelligence for any IP address or hostname: continent, country, region, city, coordinates, timezone, currency, ASN, the company the address block is registered to, plus connection flags (is_vpn, is_proxy, is_tor, is_hosting). Use my as the address to look up the calling IP.
Location is an estimate. Coordinates are the centroid of the matched city, region or country from an IP database, not a device position, and location.precision says which of the three it was. City-level matches are typically right to the metro area, not the street; region- and country-level matches can be hundreds of kilometres off. Addresses behind VPNs, mobile carriers and corporate gateways resolve to the exit point, not the user.
8.8.8.8, google.com). Use my for your own IP.curl "https://ipaddress.to/api/lookup/8.8.8.8"
const res = await fetch('https://ipaddress.to/api/lookup/8.8.8.8'); const data = await res.json(); console.log(data);
import requests data = requests.get('https://ipaddress.to/api/lookup/8.8.8.8').json() print(data)
$data = json_decode(
file_get_contents('https://ipaddress.to/api/lookup/8.8.8.8'),
true
);
print_r($data);using var client = new HttpClient(); var json = await client.GetStringAsync("https://ipaddress.to/api/lookup/8.8.8.8"); Console.WriteLine(json);
resp, _ := http.Get("https://ipaddress.to/api/lookup/8.8.8.8") body, _ := io.ReadAll(resp.Body) fmt.Println(string(body))
require 'net/http' require 'json' data = JSON.parse(Net::HTTP.get(URI('https://ipaddress.to/api/lookup/8.8.8.8'))) puts data
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://ipaddress.to/api/lookup/8.8.8.8")).build();
String body = client.send(req, BodyHandlers.ofString()).body();
System.out.println(body);use LWP::Simple; use JSON; my $data = decode_json(get('https://ipaddress.to/api/lookup/8.8.8.8')); print $data;
let body = reqwest::get("https://ipaddress.to/api/lookup/8.8.8.8") .await?.text().await?; println!("{}", body);
{
"success": true,
"ip": "8.8.8.8",
"type": "IPv4",
"rdns": "dns.google",
"location": {
"continent": "NA",
"continent_name": "North America",
"country": "United States",
"country_code": "US",
"state": "California",
"city": "Mountain View",
"latitude": 37.422,
"longitude": -122.085,
"precision": "city",
"timezone": "America/Los_Angeles",
"is_eu_member": false,
"calling_code": "1",
"capital": "Washington, D.C.",
"languages": "English",
"country_tld": ".us",
"currency": { "code": "USD", "name": "US Dollar" }
},
"asn": {
"asn": 15169,
"descr": "Google LLC",
"country": "US",
"org": "Google LLC"
},
"company": {
"name": "Google LLC",
"domain": "google.com",
"type": "hosting",
"network": "8.8.8.0/24",
"source": "registry"
},
"is_vpn": false,
"is_proxy": false,
"is_tor": false,
"is_hosting": false,
"is_private": false,
"is_cgnat": false,
"vpn_provider": null,
"duration_ms": 12
}company is the organisation the address block is registered to, taken from the regional registry (RIR) and cached per netblock — for an address inside a cloud provider’s range this is the tenant, not the provider. company.source is registry when the block comes from a registry record and asn when only the ASN name was available. company.type classifies the network (hosting / isp / business / education / government); is_hosting is the visitor-oriented flag and stays false for major providers’ own verified infrastructure (Google DNS, Cloudflare, …) even though their network type is hosting. Private, reserved and CGNAT addresses return the same shape with is_private / is_cgnat set and location.country = Private/Reserved.
WHOIS
Registration data for a domain or IP address, assembled from the authoritative registries and registrars. Domains return registrar, creation/expiry dates, status codes, nameservers and abuse contacts; IPs return network range, RIR and holder details. Subdomains are automatically stripped to the registrable root domain.
google.com, 1.1.1.1).curl "https://ipaddress.to/api/whois/google.com"
const res = await fetch('https://ipaddress.to/api/whois/google.com'); const data = await res.json(); console.log(data);
import requests data = requests.get('https://ipaddress.to/api/whois/google.com').json() print(data)
$data = json_decode(
file_get_contents('https://ipaddress.to/api/whois/google.com'),
true
);
print_r($data);using var client = new HttpClient(); var json = await client.GetStringAsync("https://ipaddress.to/api/whois/google.com"); Console.WriteLine(json);
resp, _ := http.Get("https://ipaddress.to/api/whois/google.com") body, _ := io.ReadAll(resp.Body) fmt.Println(string(body))
require 'net/http' require 'json' data = JSON.parse(Net::HTTP.get(URI('https://ipaddress.to/api/whois/google.com'))) puts data
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://ipaddress.to/api/whois/google.com")).build();
String body = client.send(req, BodyHandlers.ofString()).body();
System.out.println(body);use LWP::Simple; use JSON; my $data = decode_json(get('https://ipaddress.to/api/whois/google.com')); print $data;
let body = reqwest::get("https://ipaddress.to/api/whois/google.com") .await?.text().await?; println!("{}", body);
{
"success": true,
"query": "google.com",
"type": "domain",
"found": true,
"parsed": {
"domain_name": "GOOGLE.COM",
"registrar": "MarkMonitor Inc.",
"registrar_url": "https://rdap.markmonitor.com/rdap/domain/GOOGLE.COM",
"registrar_iana_id": "292",
"creation_date": "1997-09-15T04:00:00Z",
"expiry_date": "2028-09-14T04:00:00Z",
"updated_date": "2026-07-01T02:29:57Z",
"status": ["client delete prohibited", "client transfer prohibited"], // … 6 in total
"nameservers": ["ns1.google.com", "ns2.google.com"], // … 4 in total
"dnssec": "unsigned",
"abuse_email": "abusecomplaints@markmonitor.com",
"abuse_phone": "tel:+1.2086851750"
},
"raw": "… full registry response as a string …",
"for_sale": { "rfc": "RFC 10023", "listed": false, "checked": true, "node": "_for-sale.google.com", "text": [], "uris": [], "prices": [], "codes": [] },
"source": "registry",
"duration_ms": 342
}The raw field contains the complete unparsed registry payload; source is always registry. For domains, for_sale reports the RFC 10023 _for-sale TXT node: listed, plus any text, uris (http, https, mailto, tel only), prices (currency + amount) and codes published by the holder. Prices are indicative only. The same object is returned by /api/lookup for hostnames, /api/email for domains and /api/resolve.
Hostname Resolution
Resolve a hostname to its A (IPv4), AAAA (IPv6) and CNAME records, with automatic reverse-DNS lookups for every resolved IP.
github.com).curl "https://ipaddress.to/api/resolve/github.com"
const res = await fetch('https://ipaddress.to/api/resolve/github.com'); const data = await res.json(); console.log(data);
import requests data = requests.get('https://ipaddress.to/api/resolve/github.com').json() print(data)
$data = json_decode(
file_get_contents('https://ipaddress.to/api/resolve/github.com'),
true
);
print_r($data);using var client = new HttpClient(); var json = await client.GetStringAsync("https://ipaddress.to/api/resolve/github.com"); Console.WriteLine(json);
resp, _ := http.Get("https://ipaddress.to/api/resolve/github.com") body, _ := io.ReadAll(resp.Body) fmt.Println(string(body))
require 'net/http' require 'json' data = JSON.parse(Net::HTTP.get(URI('https://ipaddress.to/api/resolve/github.com'))) puts data
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://ipaddress.to/api/resolve/github.com")).build();
String body = client.send(req, BodyHandlers.ofString()).body();
System.out.println(body);use LWP::Simple; use JSON; my $data = decode_json(get('https://ipaddress.to/api/resolve/github.com')); print $data;
let body = reqwest::get("https://ipaddress.to/api/resolve/github.com") .await?.text().await?; println!("{}", body);
{
"success": true,
"hostname": "github.com",
"a": ["140.82.113.4"],
"aaaa": [],
"cname": [],
"mx": [{ "host": "github-com.mail.protection.outlook.com", "priority": 0 }],
"ns": ["dns1.p08.nsone.net", "dns2.p08.nsone.net"],
"txt": ["v=spf1 ip4:192.30.252.0/22 include:_spf.google.com ~all"],
"for_sale": { "rfc": "RFC 10023", "node": "_for-sale.github.com" },
"reverse": {
"140.82.113.4": "lb-140-82-113-4-iad.github.com"
},
"total_ips": 1,
"has_ipv4": true,
"has_ipv6": false,
"duration_ms": 87
}DNS Propagation
Query a DNS record against 32 public resolvers around the world and watch propagation in real time. Results are streamed as NDJSON — newline-delimited JSON, one object per line — so you can render each server's answer as it arrives. Pass ?server=IP to query a single resolver and get one plain JSON object instead.
A, AAAA, CNAME, MX, NS, TXT, SOA, PTR, SRV, CAA (case-insensitive).example.com).?server=8.8.8.8. Returns a single JSON object instead of a stream.curl -N "https://ipaddress.to/api/dns/a/example.com"
const res = await fetch('https://ipaddress.to/api/dns/a/example.com'); const reader = res.body.getReader(); // read the stream — each line is one JSON object
import requests, json r = requests.get('https://ipaddress.to/api/dns/a/example.com', stream=True) for line in r.iter_lines(): print(json.loads(line))
{"type":"start","domain":"example.com","recordType":"A","total":32}
{"type":"result","index":0,"result":{"status":"ok","value":"104.20.23.154","ttl":133,"elapsed":71,"raw":[{"name":"example.com","ttl":133,"class":"IN","type":"A","value":"104.20.23.154"}]}}
// … one "result" line per server (32 total), in server-list order …
{"type":"done","summary":{"successful":29,"failed":3,"total":32}}The stream opens with a start object, emits one result object per server (with index, answer value, ttl, latency elapsed in ms and the raw record set), and closes with a done summary.
{
"success": true,
"domain": "example.com",
"type": "A",
"server": { "ip": "8.8.8.8", "name": "New York", "country": "US", "provider": "Google" },
"result": {
"status": "ok",
"value": "104.20.23.154",
"ttl": 300,
"elapsed": 41,
"raw": [
{ "name": "example.com", "ttl": 300, "class": "IN", "type": "A", "value": "104.20.23.154" },
{ "name": "example.com", "ttl": 300, "class": "IN", "type": "A", "value": "172.66.147.243" }
]
}
}IP Fraud Score
A 0–100 risk score built from threat blocklists, VPN/proxy/Tor detection, ASN classification, reverse-DNS heuristics and bot signals. risk is one of low, medium, high or critical, and factors explains exactly which signals moved the score. Known crawler IPs (Googlebot, Ahrefs, …) are verified via forward-confirmed reverse DNS and flagged rather than penalised.
What the number means. The score is the probability that the connection is not ordinary consumer traffic: VPN, proxy, Tor, datacenter, or infrastructure listed for abuse. It knows nothing about the person or their intent, and it is not calibrated against chargeback or fraud outcomes, so a score of 80 is not an 80 % chance of fraud. The response repeats this in meaning. Before letting the score gate real customers, measure false positives and misses on a sample of your own traffic, and treat a low score as "no infrastructure signal", not as proof of a good actor.
The endpoint scores two different things depending on what you pass, reported back as target_type. An IP address (target_type: "ip") is scored as a visitor — "is traffic from this address suspicious?" — so VPN/proxy/Tor, datacenter hosting, missing reverse DNS and crawler signals all count against it. A domain (target_type: "domain") is resolved and scored as a website — "is this site dangerous?" — using only server-reputation signals (threat intel, bulletproof/shady hosting, bogons); being hosted in a datacenter or on a CDN is normal for a server and is not penalised. In domain mode the visitor-only categories in score_breakdown (anonymity, infrastructure, bot) are null — not evaluated, as opposed to 0 (evaluated and clean).
Threat-intel listings are weighted by feed reliability, de-duplicated across feeds that republish the same data, and discounted when a feed has stopped updating. On residential and mobile networks (checks.is_isp / checks.is_mobile), where one address is shared and reassigned among many people, non-critical listings are discounted further because they usually describe a previous holder. Tor exit relays (tor_exit) are scored as anonymised visitors; guard/middle relays (tor_relay) only forward Tor traffic and get a much smaller penalty. A VPN provider is only named in vpn_provider when the exact address is a listed endpoint; an address that merely shares a /24 with endpoints is reported as checks.vpn_neighbor with a small weight.
my for your own IP.curl "https://ipaddress.to/api/score/8.8.8.8"
const res = await fetch('https://ipaddress.to/api/score/8.8.8.8'); const data = await res.json(); console.log(data);
import requests data = requests.get('https://ipaddress.to/api/score/8.8.8.8').json() print(data)
$data = json_decode(
file_get_contents('https://ipaddress.to/api/score/8.8.8.8'),
true
);
print_r($data);using var client = new HttpClient(); var json = await client.GetStringAsync("https://ipaddress.to/api/score/8.8.8.8"); Console.WriteLine(json);
resp, _ := http.Get("https://ipaddress.to/api/score/8.8.8.8") body, _ := io.ReadAll(resp.Body) fmt.Println(string(body))
require 'net/http' require 'json' data = JSON.parse(Net::HTTP.get(URI('https://ipaddress.to/api/score/8.8.8.8'))) puts data
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://ipaddress.to/api/score/8.8.8.8")).build();
String body = client.send(req, BodyHandlers.ofString()).body();
System.out.println(body);use LWP::Simple; use JSON; my $data = decode_json(get('https://ipaddress.to/api/score/8.8.8.8')); print $data;
let body = reqwest::get("https://ipaddress.to/api/score/8.8.8.8") .await?.text().await?; println!("{}", body);
{
"success": true,
"ip": "8.8.8.8",
"target_type": "ip",
"score": 3,
"risk": "low",
"meaning": "Probability that this address is non-consumer or abused infrastructure (VPN, proxy, Tor, hosting, blocklisted). It is not the probability that the person behind it is committing fraud.",
"score_breakdown": { "threat": 0, "anonymity": 0, "infrastructure": 25, "reputation": 0, "bot": 0 },
"vpn_provider": null,
"rdns": "dns.google",
"ip_version": 4,
"location": { "country": "United States", "country_code": "US", "city": "Mountain View" },
"asn": { "asn": 15169, "org": "Google LLC" },
"company": { "name": "Google LLC", "type": "trusted" },
"checks": {
"trusted_org": true,
"has_rdns": true,
"is_bot": false,
"vpn_detected": false,
"tor_exit": false,
"tor_relay": false,
"is_mobile": false,
"commercial_proxy": false,
"blocklist_listed": false,
"blocklist_count": 0,
"hosting_asn": true,
"asn_type": "hosting",
"high_risk_country": false
// … 26 checks in total (vpn_neighbor and vpn_provider appear only when detected)
},
"factors": [
{ "name": "Trusted Organization", "impact": "positive", "detail": "IP belongs to Google infrastructure (verified via ASN + rDNS)" },
{ "name": "Datacenter / Hosting", "impact": "medium", "detail": "AS15169 (Google LLC) is hosting/datacenter infrastructure" }
],
"duration_ms": 60
}Email Check
Trust score (0–100, higher is better) for an email address or a domain. Deliverability: MX records that actually resolve (null MX and dangling MX are detected), the domain resolves, and — for an address — a live SMTP RCPT TO probe against the primary mail server reported in checks.mailbox as deliverable, undeliverable, catch_all (server accepts every address) or unknown (greylisted, blocked or timed out); no message is ever sent. Authentication: SPF policy strength (spf_policy: hardfail > softfail > redirect > neutral > open > permissive; several records = invalid), DMARC p= policy with pct and organisational-domain inheritance for subdomains, and a DKIM key found under a common selector. Reputation: disposable providers (by name, or by delivering to a known disposable provider’s mail server — disposable_via_mx), spam-associated domains, high-abuse TLDs, parked / for-sale domains and machine-generated local parts. Unregistered domains (domain_exists: false) are capped at 5. normalized_email strips +tags and, for Gmail, dots.
you@gmail.com) or domain (e.g. mailinator.com). Omit entirely to get a random sample of known disposable domains.curl "https://ipaddress.to/api/email/test@gmail.com"
const res = await fetch('https://ipaddress.to/api/email/test@gmail.com'); const data = await res.json(); console.log(data);
import requests data = requests.get('https://ipaddress.to/api/email/test@gmail.com').json() print(data)
$data = json_decode(
file_get_contents('https://ipaddress.to/api/email/test@gmail.com'),
true
);
print_r($data);using var client = new HttpClient(); var json = await client.GetStringAsync("https://ipaddress.to/api/email/test@gmail.com"); Console.WriteLine(json);
resp, _ := http.Get("https://ipaddress.to/api/email/test@gmail.com") body, _ := io.ReadAll(resp.Body) fmt.Println(string(body))
require 'net/http' require 'json' data = JSON.parse(Net::HTTP.get(URI('https://ipaddress.to/api/email/test@gmail.com'))) puts data
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://ipaddress.to/api/email/test@gmail.com")).build();
String body = client.send(req, BodyHandlers.ofString()).body();
System.out.println(body);use LWP::Simple; use JSON; my $data = decode_json(get('https://ipaddress.to/api/email/test@gmail.com')); print $data;
let body = reqwest::get("https://ipaddress.to/api/email/test@gmail.com") .await?.text().await?; println!("{}", body);
{
"success": true,
"query": "test@gmail.com",
"email": "test@gmail.com",
"domain": "gmail.com",
"target_type": "email",
"normalized_email": "test@gmail.com",
"apex_domain": "gmail.com",
"score": 10,
"rating": "high-risk",
"score_breakdown": { "deliverability": 35, "authentication": 24, "reputation": 20 },
"checks": {
"domain_exists": true,
"has_mx": true,
"null_mx": false,
"mx_hosts": ["gmail-smtp-in.l.google.com", "alt1.gmail-smtp-in.l.google.com"],
"mx_resolves": true,
"spf": true,
"spf_policy": "redirect",
"spf_record": "v=spf1 redirect=_spf.google.com",
"dmarc": true,
"dmarc_policy": "none",
"dmarc_pct": 100,
"dmarc_record": "v=DMARC1; p=none; sp=quarantine; rua=mailto:mailauth-reports@google.com",
"dmarc_inherited": false,
"dkim": false,
"dkim_selector": null,
"resolves": true,
"disposable": false,
"disposable_via_mx": null,
"role_based": true,
"free_provider": true,
"spam_listed": false,
"abused_tld": false,
"random_local": false,
"subaddress": null,
"parked": false,
"mailbox": "undeliverable",
"smtp_code": 550,
"smtp_message": "5.1.1 The email account that you tried to reach does not exist.",
"smtp_mx": "gmail-smtp-in.l.google.com"
},
"is_disposable": false,
"factors": [
{ "name": "Mailbox Rejected", "impact": "negative", "detail": "The domain's mail server rejected this address (550 5.1.1 …) — the mailbox does not exist" },
{ "name": "Mail Server (MX)", "impact": "positive", "detail": "Domain has 5 MX records (gmail-smtp-in.l.google.com) — it can receive email" },
{ "name": "Role-based Address", "impact": "negative", "detail": "\"test@\" is a role/group mailbox, not an individual person" }
// … one entry per signal
],
"duration_ms": 168
}Discovery Endpoint
GET https://ipaddress.to/api/ returns a machine-readable index of every endpoint — handy for tooling and quick reference from the terminal. (Browsers requesting HTML are redirected to this page instead.)
{
"name": "IPAddress.to API",
"version": "1.1",
"docs": "https://ipaddress.to/api/docs/",
"endpoints": [
{
"path": "/api/lookup/{ip_or_hostname}",
"method": "GET",
"description": "Geolocation and network info for an IP address or hostname…",
"examples": ["/api/lookup/8.8.8.8", "/api/lookup/google.com", "/api/lookup/my"]
}
// … one entry per endpoint
]
}Errors & Caching
Failed lookups return a JSON body with success: false and a human-readable error message — always branch on the success field rather than the HTTP status alone.
{
"success": false,
"error": "Could not resolve hostname: 999.1.1.1",
"query": "999.1.1.1",
"duration_ms": 53
}Cache-Control headers; cached responses include "cached": true. If you need the same answer repeatedly, cache it on your side too.Rate Limits & Fair Use
The API is free by design: the lookups people run are what tell us where our classification data has gaps, and fixing those gaps is how the scores get more accurate. The lookups themselves are never sold or shared with third parties. There is no hard quota — the API is free for fair use, and the same wording applies on the homepage: no paid tiers, no per-key limits today. Please be a good citizen: cache results (responses carry a 60-second cache), avoid tight polling loops, and identify your application with a descriptive User-Agent header. Traffic that looks like bulk scraping or sustained high-volume automation is throttled at the edge; if you expect sustained volume, tell us first so we can make room for it rather than throttle it. The service is provided without an uptime or accuracy guarantee (see Terms), so production integrations should use timeouts, caching and a fallback. Sending an API key with your requests lets us distinguish your traffic from anonymous noise — and is the path to higher limits if quotas are ever introduced. Need sustained high volume? Get in touch.
Internal / Experimental Endpoints
The browser proxy-detection widget on the homepage is powered by three additional endpoints: POST /api/detect/fingerprint (HTTP-layer request fingerprint), GET /api/detect/latency (RTT probe) and POST /api/detect/score (combined proxy/VPN score from client + server signals). They are internal: request/response shapes are tightly coupled to the widget and may change at any time without notice — build against the stable endpoints above instead.