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.

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.

What comes back, and what it is not

Responses from this API contain no Chinese. Malware-list hits and the country/ASN of each resolved IP come back as standard codes or stable slugs.

FieldWhat it means
data[] The resolution records. An empty array is an answer, not an error — "data": [] means no matching records. Bad parameters never reach it: invalid input returns 400 and says which parameter was wrong. So automation can branch safely on status code.
corpus_last_seen The snapshot cutoff date of the current corpus. Top-level. You must read it to judge record age; do not hard-code a date (see the previous section).
profile{} The domain profile object (this endpoint only; the certificate endpoint has none). Four parts: ip_enrichment[] carries country / city / ASN and ASN organisation for each resolved IP; malware{} carries malware-list hits (listed, last_listed, days_listed); record_types{} is the record-type breakdown; plus nameservers[] and mx[]. The "country / city / ASN" and "malware-list hits" rows below live inside this object. whois is currently always null — we do not hold registrant data, so do not build on it.
domain / query Echo of what was actually queried. Domains accept an ASCII character set only (letters, digits, dots, hyphens, underscores); convert an internationalised domain to punycode (starting with xn--) yourself before querying — punycode is plain ASCII and passes.
The domain ↔ IP relationship Which IPs the domain resolved to, with the first and last date each relationship appeared in the corpus. All six record types are included: A, AAAA, CNAME, MX, NS and TXT — so changes in mail and authoritative-server configuration are in there too, not just A records.
Country / city / ASN per resolved IP From the same call. This is the pivot for expansion: see which machine the domain landed on, then look up what else sat on that machine.
Malware-list hits Whether the domain appears on the malware lists we track, with the most recent hit and the cumulative number of days. For merchant verification and abuse complaints, without stitching together a second data source.
has_more / next_offset Paging. Page with offset (?offset=60): this endpoint has no limit — the page size is server-side and not yours to set; passing limit is ignored in practice. count is how many records this response carries and shrinks with offset (a 64-record result at offset=60 comes back with 4). To decide whether another page exists, read has_more only.

Times are UTC, not Beijing time

Every time field is UTC. Add 8 hours yourself before aggregating by day. Getting this wrong yields an empty result on small windows and then looks like the feature is broken.

Passive DNS is an observation, not an authority

We record resolution relationships that were observed. Anything unobserved is absent — a niche domain, a freshly registered one, or one resolved only in a specific region may simply never have been seen by us. That is a coverage blind spot, not a clean bill of health. Treat "queried and found nothing risky" differently from "queried and found nothing at all", and do not handle both in the same branch.

Where it gets used

Clustering fraud domains

Take a suspicious domain, see which IP it landed on, then look up what else historically sat on that IP. Infrastructure run at volume rarely puts one domain on one IP — we have measured over a thousand domains behind a single CDN IP.

Incubation signal for phishing infrastructure

The intersection of "registered today" and "got a certificate today" is infrastructure still being assembled, not yet in operation. That is the window where you get there before the victims do.

Domain due diligence and attribution

One call returns resolution history, malware-list hits and the country, city and ASN of every resolved IP. The first step of merchant verification, abuse evidence-gathering or incident attribution.

Retracing what an IP hosted

Run it in reverse: when an IP is flagged as malicious, see which domains it historically carried, to bound the blast radius and find same-origin assets.

Used together with certificate transparency

The two make one complete chain. Certificate transparency tells you someone just got a certificate for a look-alike domain — the earliest signal, usually before the domain resolves, before content goes up, and before anyone reports it. Passive DNS tells you which machine that domain landed on and who else is on it. Either one alone shows you half the picture.

See certificate transparency →

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.

Coverage and freshness

Coverage

Covering A, AAAA, CNAME, MX, NS and TXT, each with a first-seen and last-seen date. The corpus is imported in periodic batches; its cutoff date is corpus_last_seen at the top level of the response.

Latency moves during the backfill

A background backfill job competes for the same CPU and disk as queries, so latency moves between 1 and 5 seconds against a baseline of about one second. This is not a failure; it settles back to a stable one-second range once the backfill finishes.

⚠️ The paragraph above is about forward lookup (which addresses a domain used). Reverse lookup (IP → domains) is open too: see GET /v1/domain/reverse/{ip}. It is a full-table scan, and we wait at most 25 seconds for it.

The boundary: how far reverse lookup is open

Forward lookup (domain → IP) asks about one domain's own history. Reverse lookup (IP → the list of domains) runs the other way — it amounts to a ready-made asset map, so this is where we draw the line (up to 1000 rows per call, and it does paginate):

ContentPublic?Why
One domain's resolution history (IPs, first/last seen, record types) Yes It is the question the product exists to answer, and it concerns only that one domain's history.
Aggregates: how many IPs it resolved to, how long relationships lasted Yes Enough to answer "is this domain's hosting stable?".
The domains on one IP (reverse lookup, page by page) Yes Up to 1000 rows at a time, continue with ?offset=. It is a full-table scan, slower than forward lookup (seconds), and we wait at most 25 seconds for it.
A one-shot bulk export (every domain on an IP, or on a batch of IPs) Not offered by this API The API serves pages; there is no export action. To feed this into your own network or to monitor at bulk, please contact sales.
The complete list of newly registered domains for one day Not offered by this API That list is sorted by domain and served 1000 rows at a time, with no usable paging parameter; the rest of the day is not available from this API. For the full day, please contact us.
Data sources and source names Never Source composition is commercial information and is not disclosed to any customer.

We do not probe actively, and we do not brute-force subdomains

This API does not issue active DNS lookups, scan subdomains or run brute-force enumeration. It reports only what our passive observation saw. For subdomain information, go to the certificate transparency logs — a different data source with a different boundary.

Contact sales

For bulk domain monitoring, for feeding reverse-lookup results into your own network, or for the full day of newly registered domains, tell us the use case and roughly what volume you expect.

Fill in the contact form →