Six record types · Periodic snapshots · Free trial queries
Passive DNS
Give it a domain and it returns which IPs that domain resolved to historically, and how long each relationship lasted. Or run it the other way round: what else sits on a given IP.
An ordinary DNS lookup only tells you where a domain points right now. Adversary infrastructure rotates domains, IPs and registrars — but it cannot rotate away relationships that already happened. A taken-down phishing domain often shares its server, from three months ago, with dozens more domains belonging to the same crew.
| Record type | Value | Owner | First seen | Last seen |
Raw JSON (the same response integrators get)
Key-free trial: 10 lookups per IP per day, and signing in does not raise it. The box takes either a domain (forward: which addresses did it use) or an IP address (reverse: which domains sit on it) — both endpoints share this quota. This box calls the endpoint in the Access section below and returns the same response an integrator gets.
First, what last_seen actually means
This section comes first because it is the single easiest field on this product to misread — and misreading it leads to the opposite conclusion.
last_seen is the corpus snapshot date
Our resolution corpus is imported as periodic batch snapshots. So if you query any active domain today, its last_seen will land on that snapshot's date — seeing a date from a month ago does not mean the domain went offline. It only means the corpus kept observing this relationship in that period.
So read corpus_last_seen
The top-level corpus_last_seen is the corpus cutoff date. To judge how old a record is, read last_seen together with it. Read that field; do not hard-code a date.
One other pipeline — newly registered domains for a given day — updates daily and is not subject to this. The two have different time semantics; do not apply the same explanation to both.
Integration
Free trial queries, callable without an account. Response fields, quotas and error formats are in the API documentation; for the certificate side see certificate transparency.
GEThttps://ip99.com/v1/domain/dns/{domain}
# Call it directly, no key required (anonymous quota)
curl https://ip99.com/v1/domain/dns/example.com
# Page with offset (this endpoint has no limit; page size is server-side)
curl "https://ip99.com/v1/domain/dns/example.com?offset=60"
Lower-case the domain, strip the trailing dot, and use punycode for non-ASCII names. To decide whether another page exists, read has_more only; offset takes a non-negative integer and anything else returns 400. While a background backfill job is running, the same query takes 1–5 seconds (baseline about 1s). That is not a failure.
GEThttps://ip99.com/v1/domain/registered/{date}
# Newly registered domains for one day, sorted by domain, also keyless
curl https://ip99.com/v1/domain/registered/2026-10-08
This is the “updates daily” pipeline mentioned above. {date} must be a real calendar date in YYYY-MM-DD form: 2026-13-45 returns 400 rather than silently an empty list. Each record carries domain, ip, registrar and country. This endpoint does not page: one call returns the first 1000 rows sorted by domain, and passing offset returns 400 pagination_unsupported. The response therefore has no has_more or next_offset — do not loop on them. A date with no data yet returns an empty array (an answer, not an error).
GEThttps://ip99.com/v1/domain/malware/{domain}
# Malware-list check: the dates this domain was listed, keyless
curl https://ip99.com/v1/domain/malware/starglobalbank.com
The same data as profile.malware in the DNS response, only far cheaper to ask (milliseconds, so it fits per-item pipeline checks): here you get the list of listing dates, while the profile gives the summary (listed, last_listed, days_listed). A hit and a miss are both HTTP 200 — judge by whether data is empty, not by the status code. This endpoint carries no pagination fields either.
GEThttps://ip99.com/v1/domain/reverse/{ip}
# Reverse IP lookup: the domains this address historically resolved, keyless
curl "https://ip99.com/v1/domain/reverse/104.21.5.141"
Forward lookup answers "which addresses did this name use"; reverse lookup answers the opposite question from the same passive-DNS corpus. Each row is domain, rrtype, first_seen, last_seen; up to 1000 rows per call, and it does paginate (read has_more and next_offset, continue with ?offset=N). {ip} takes a bare address only (8.8.8.8 or 2606:4700:4700::1111): a form like 8.8.8.8:443 or 8.8.8.8/32 is rejected — strip the port and the netmask, then retry. Private addresses are not an error — values like 10.0.0.1 really do appear in DNS records. It is a full-table scan, so we wait at most 25 seconds; it shares the same keyless trial quota as the cards above.