who.is Domain Intelligence API

Get parsed WHOIS records, normalized RDAP data, DNS records, and TLS certificate details for any domain, all as clean JSON over HTTPS. Because we keep the history, you can also see what changed and when, from a domain moving to a new registrar to a swapped set of nameservers or a fresh certificate. Zone feeds let you watch a whole TLD day by day and catch new registrations, nameserver moves, and drops as they happen. Start free with no card, and build anything from a quick lookup tool to a brand monitor to a research pipeline over millions of domains.

Endpoints at a glance

Quickstart

Every request is authenticated with your key as a Bearer token. Create a key on the API keys page, then:

curl https://api.who.is/v1/whois/example.com \
  -H "Authorization: Bearer wis_live_..."

Authentication

Send your key in the Authorization header: Authorization: Bearer wis_live_<key>. Keys are shown once at creation — store yours securely. A missing or invalid key returns 401 unauthorized. Never put the key in a query string.

Plans & quotas

PlanPriceRequests / monthRequests / dayRequests / secondFull-history / monthLive refreshes / day
Free$0500100100
Pro API Access$29.00/mo15,000No daily cap55500
Growth API Access$99.00/mo60,000No daily cap5101000
Scale API Access$199.00/mo150,000No daily cap5202000

Every authenticated call counts as one request. ?live=true (forcing a fresh upstream lookup) and full-history queries each additionally decrement their own counter. Quotas are hard caps — there is no metered overage. DNS and certificate change timelines (/changes) require a paid plan — current-state DNS and certificate lookups are on every plan; on Free the timelines return 403 plan_required.

Endpoints

GET/v1/whois/{domain}

Snapshot-first parsed WHOIS: registrar, statuses, events, nameservers, and redacted-as-received contacts. Serves a stored snapshot (≤30 days) unless `?live=true` forces an upstream refresh. Raw upstream text is never returned.

Parameters

NameInRequiredDescription
domainpathyesThe domain to look up, e.g. `example.com`. A subdomain is resolved to its registrable domain (`www.example.com` → `example.com`).
livequerynoSet to `true` to force a fresh upstream lookup (bounded by a 1-hour floor). Starter-only — a free key is denied with `plan_required`. Any other value is a normal snapshot lookup.

Example response

{
  "domain": "example.com",
  "registrar": "Example Registrar, Inc.",
  "whois_server": "whois.example-registrar.com",
  "referral_url": "https://example-registrar.com",
  "statuses": [
    "clientTransferProhibited"
  ],
  "snapshot_time": "2026-07-30T12:00:00.000Z",
  "events": [
    {
      "event_action": "registration",
      "event_date": "2003-01-01T00:00:00.000Z"
    },
    {
      "event_action": "expiration",
      "event_date": "2027-01-01T00:00:00.000Z"
    }
  ],
  "nameservers": [
    {
      "ldh_name": "ns1.example.com"
    },
    {
      "ldh_name": "ns2.example.com"
    }
  ],
  "contacts": {
    "registrant": {
      "organization": "Acme",
      "state_province": "CA",
      "country": "US",
      "email": "jane@acme.test"
    }
  }
}

Errors

invalid_domain (400) · unauthorized (401) · domain_not_available (404) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503) · plan_required (403) · not_found (404) · upstream_unavailable (502)

GET/v1/rdap/{domain}

Snapshot-first normalized RDAP: entities (recursing through sub-entities), events, and nameservers. Serves a stored snapshot unless `?live=true` forces an upstream refresh. Never an upstream passthrough.

Parameters

NameInRequiredDescription
domainpathyesThe domain to look up, e.g. `example.com`. A subdomain is resolved to its registrable domain (`www.example.com` → `example.com`).
livequerynoSet to `true` to force a fresh upstream lookup (bounded by a 1-hour floor). Starter-only — a free key is denied with `plan_required`. Any other value is a normal snapshot lookup.

Example response

{
  "domain": "example.com",
  "handle": "DOM-123",
  "object_class_name": "domain",
  "statuses": [
    "active"
  ],
  "rdap_conformance": [
    "rdap_level_0"
  ],
  "resource_url": "https://rdap.example/domain/example.com",
  "snapshot_time": "2026-07-30T12:00:00.000Z",
  "entities": [
    {
      "handle": "REG-1",
      "roles": [
        "registrar"
      ],
      "fn": "Example Registrar",
      "org": "Example Registrar, Inc.",
      "country_code": "US",
      "public_id_type": "IANA Registrar ID",
      "public_id": "9999",
      "child_entities": [
        {
          "handle": "ABUSE-1",
          "roles": [
            "abuse"
          ],
          "email": "abuse@example.test"
        }
      ]
    }
  ],
  "events": [
    {
      "event_action": "registration",
      "event_date": "2003-01-01T00:00:00.000Z"
    }
  ],
  "nameservers": [
    {
      "ldh_name": "ns1.example.com"
    }
  ]
}

Errors

invalid_domain (400) · unauthorized (401) · domain_not_available (404) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503) · plan_required (403) · not_found (404) · upstream_unavailable (502)

GET/v1/domains/{domain}/history

A domain's history at a glance: snapshot, change, and registrar counts plus the first/last-seen window, drawn from our archive of 2B+ WHOIS/RDAP snapshots going back to 2003. Available on every plan; the full per-snapshot history is a separate endpoint.

Parameters

NameInRequiredDescription
domainpathyesThe domain to look up, e.g. `example.com`. A subdomain is resolved to its registrable domain (`www.example.com` → `example.com`).

Example response

{
  "domain": "example.com",
  "snapshot_count": 2741,
  "whois_snapshot_count": 2000,
  "rdap_snapshot_count": 741,
  "change_count": 58,
  "registrar_count": 4,
  "first_seen": "2003-01-01T00:00:00.000Z",
  "last_seen": "2026-07-01T00:00:00.000Z",
  "updated_at": "2026-07-30T00:00:00.000Z"
}

Errors

invalid_domain (400) · unauthorized (401) · domain_not_available (404) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503) · not_found (404)

GET/v1/domains/{domain}/history/snapshots

The complete per-snapshot history from the 2B+ snapshot archive: every parsed WHOIS/RDAP snapshot (facts only — never raw text) plus the field-diff change timeline. Starter-only; a free key is denied with `plan_required`. This endpoint is **async-or-cached** and does not run the archive query on the request path. A prepared result is served immediately (`200`); otherwise the call returns `202 { "status": "preparing", "retry_after": 10 }` with a `Retry-After` header — poll until it returns `200`. If preparation failed, `502 upstream_unavailable` is returned and the next call re-queues. **Charge-once:** the first request that enqueues (or serves) a given domain's history spends your scarce full-history quota; subsequent polls and re-reads of the same domain (within the same archive generation) spend the ordinary per-request quota instead. **Errors are never charged:** a `503` (queue/metering unavailable) refunds the request, and a failed preparation (`502`) additionally refunds the full-history credit you paid — retrying after a `502` starts (and charges) fresh.

Parameters

NameInRequiredDescription
domainpathyesThe domain to look up, e.g. `example.com`. A subdomain is resolved to its registrable domain (`www.example.com` → `example.com`).

Example response

{
  "domain": "example.com",
  "generation": 1,
  "snapshot_count": 3,
  "change_count": 1,
  "registrar_count": 2,
  "first_seen": "2003-01-01T00:00:00.000Z",
  "last_seen": "2026-07-01T00:00:00.000Z",
  "snapshots": [
    {
      "snapshot_time": "2003-01-01T00:00:00.000Z",
      "protocol": "whois",
      "source": "registrar",
      "registrar": "Example Registrar, Inc.",
      "registrar_iana_id": "9999",
      "created": "2003-01-01",
      "updated": "2012-05-10",
      "expires": "2027-01-01",
      "name_servers": [
        "ns1.example.com",
        "ns2.example.com"
      ],
      "statuses": [
        "clientTransferProhibited"
      ],
      "dnssec": "unsigned",
      "privacy_protected": false,
      "contacts": {
        "registrant": {
          "organization": "Acme",
          "state_province": "CA",
          "country": "US",
          "email": "jane@acme.test"
        }
      }
    }
  ],
  "changes": [
    {
      "snapshot_time": "2003-01-01T00:00:00.000Z",
      "changed_day": "2003-01-01",
      "kind": "initial",
      "changed_fields": []
    },
    {
      "snapshot_time": "2012-05-10T00:00:00.000Z",
      "changed_day": "2012-05-10",
      "kind": "change",
      "changed_fields": [
        "registrar"
      ]
    }
  ]
}

Errors

invalid_domain (400) · unauthorized (401) · domain_not_available (404) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503) · plan_required (403) · upstream_unavailable (502)

GET/v1/dns/{domain}

The zone's current records with hosting classification. Observation begins at first interest — a first query for an unobserved domain resolves it live, records the observation, and serves it; a domain with no DNS records returns `not_found`.

Parameters

NameInRequiredDescription
domainpathyesThe domain to look up, e.g. `example.com`. A subdomain is resolved to its registrable domain (`www.example.com` → `example.com`).

Example response

{
  "domain": "example.com",
  "hostnames": [
    {
      "hostname": "example.com",
      "hosting_class": "direct",
      "provider_name": null,
      "first_seen": "2025-01-01T00:00:00.000Z",
      "last_seen": "2026-07-29T00:00:00.000Z",
      "records": {
        "A": [
          "1.2.3.4"
        ],
        "NS": [
          "ns1.example.com",
          "ns2.example.com"
        ]
      }
    },
    {
      "hostname": "www.example.com",
      "hosting_class": "cdn",
      "provider_name": "Cloudflare",
      "first_seen": "2025-02-01T00:00:00.000Z",
      "last_seen": "2026-07-30T00:00:00.000Z",
      "records": {
        "CNAME": [
          "example.com"
        ]
      }
    }
  ],
  "snapshot_time": "2026-07-30T00:00:00.000Z"
}

Errors

invalid_domain (400) · unauthorized (401) · domain_not_available (404) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503) · not_found (404)

GET/v1/dns/{domain}/changes

Every recorded change to this zone's DNS records, oldest first. Entries are changes only — a record set that stayed the same does not produce one — and each entry carries what changed, when it took effect, and how long it stood. An empty timeline is a valid 200 with an empty `changes` array. `snapshot_time` describes the whole timeline we hold for the zone, not just the page in hand. Requires a paid plan: on the Free plan this endpoint returns `plan_required` (403). Pass `pagination.next_cursor` back unchanged to fetch the next page; `next_cursor: null` means you have reached the last page. Entries are only ever appended, so you can keep the last cursor and re-use it later to pick up whatever has since been observed.

Parameters

NameInRequiredDescription
domainpathyesThe domain to look up, e.g. `example.com`. A subdomain is resolved to its registrable domain (`www.example.com` → `example.com`).
limitquerynoResults per page (1–1000, default 100). A value above 1000 is clamped to 1000.
cursorquerynoOpaque cursor from a prior response's `pagination.next_cursor`. Pass it back unchanged, with the same path and query parameters, to fetch the next page. A cursor used with a different query returns 400 `invalid_cursor`.

Example response

{
  "domain": "example.com",
  "changes": [
    {
      "kind": "initial",
      "hostname": "example.com",
      "first_seen": "2024-06-01T00:00:00.000Z",
      "last_seen": "2025-01-01T00:00:00.000Z",
      "hosting_class": "direct",
      "provider_name": null,
      "current": false,
      "change_summary": null,
      "records": {
        "A": [
          "1.1.1.1"
        ]
      }
    },
    {
      "kind": "change",
      "hostname": "example.com",
      "first_seen": "2025-01-01T00:00:00.000Z",
      "last_seen": "2026-07-30T00:00:00.000Z",
      "hosting_class": "direct",
      "provider_name": null,
      "current": true,
      "change_summary": {
        "added_rrtypes": [
          "A"
        ],
        "removed_rrtypes": [],
        "changed_rrtypes": [],
        "detail": [
          "A: added 2.2.2.2"
        ]
      },
      "records": {
        "A": [
          "1.1.1.1",
          "2.2.2.2"
        ]
      }
    }
  ],
  "pagination": {
    "next_cursor": null,
    "limit": 100
  },
  "snapshot_time": "2026-07-30T00:00:00.000Z"
}

Errors

invalid_domain (400) · unauthorized (401) · domain_not_available (404) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503) · plan_required (403) · invalid_request (400) · invalid_cursor (400)

GET/v1/certificates/{domain}

The current leaf certificate for a host: issuer, subject, SANs, validity window, and structural chain status. Serves the stored certificate; a first query for an unobserved host probes it live (at most once per 24h), records the observation, and serves it. Certificates are host-specific, so a subdomain is looked up as-is.

Parameters

NameInRequiredDescription
domainpathyesThe host to look up, e.g. `example.com` or `shop.example.com`. Certificates are host-specific, so a subdomain is looked up as-is.

Example response

{
  "domain": "example.com",
  "subject_cn": "example.com",
  "issuer_ca": "Let's Encrypt",
  "issuer_cn": "R3",
  "issuer_org": "Let's Encrypt",
  "sans": [
    "example.com",
    "www.example.com"
  ],
  "serial": "AB12",
  "fingerprint_sha256": "AA:BB:CC",
  "valid_from": "2026-05-01T00:00:00.000Z",
  "valid_to": "2026-08-01T00:00:00.000Z",
  "key_algo": "EC",
  "key_bits": 256,
  "self_signed": false,
  "chain_status": "trusted",
  "chain_issuers": [
    "R3",
    "ISRG Root X1"
  ],
  "snapshot_time": "2026-07-30T00:00:00.000Z"
}

Errors

invalid_domain (400) · unauthorized (401) · domain_not_available (404) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503) · not_found (404)

GET/v1/certificates/{domain}/changes

Every certificate this host has been observed serving, oldest first — one entry per certificate, with what changed at each rotation and the window it was served in. An empty timeline is a valid 200 with an empty `changes` array. `snapshot_time` describes the whole timeline we hold for the host, not just the page in hand. Requires a paid plan: on the Free plan this endpoint returns `plan_required` (403). Pass `pagination.next_cursor` back unchanged to fetch the next page; `next_cursor: null` means you have reached the last page. Entries are only ever appended, so you can keep the last cursor and re-use it later to pick up whatever has since been observed.

Parameters

NameInRequiredDescription
domainpathyesThe host to look up, e.g. `example.com` or `shop.example.com`. Certificates are host-specific, so a subdomain is looked up as-is.
limitquerynoResults per page (1–1000, default 100). A value above 1000 is clamped to 1000.
cursorquerynoOpaque cursor from a prior response's `pagination.next_cursor`. Pass it back unchanged, with the same path and query parameters, to fetch the next page. A cursor used with a different query returns 400 `invalid_cursor`.

Example response

{
  "domain": "example.com",
  "changes": [
    {
      "kind": "initial",
      "first_seen": "2026-02-01T00:00:00.000Z",
      "last_seen": "2026-05-01T00:00:00.000Z",
      "current": false,
      "change_summary": null,
      "cert": {
        "subject_cn": "example.com",
        "issuer_ca": "Let's Encrypt",
        "issuer_cn": "R3",
        "issuer_org": "Let's Encrypt",
        "sans": [
          "example.com",
          "www.example.com"
        ],
        "serial": "AB12",
        "fingerprint_sha256": "OLD:FP",
        "valid_from": "2026-05-01T00:00:00.000Z",
        "valid_to": "2026-08-01T00:00:00.000Z",
        "key_algo": "EC",
        "key_bits": 256,
        "self_signed": false,
        "chain_status": "trusted",
        "chain_issuers": [
          "R3",
          "ISRG Root X1"
        ]
      }
    },
    {
      "kind": "change",
      "first_seen": "2026-05-01T00:00:00.000Z",
      "last_seen": "2026-07-30T00:00:00.000Z",
      "current": true,
      "change_summary": {
        "affected": [
          "fingerprint"
        ],
        "detail": [
          "fingerprint rotated"
        ]
      },
      "cert": {
        "subject_cn": "example.com",
        "issuer_ca": "Let's Encrypt",
        "issuer_cn": "R3",
        "issuer_org": "Let's Encrypt",
        "sans": [
          "example.com",
          "www.example.com"
        ],
        "serial": "AB12",
        "fingerprint_sha256": "AA:BB:CC",
        "valid_from": "2026-05-01T00:00:00.000Z",
        "valid_to": "2026-08-01T00:00:00.000Z",
        "key_algo": "EC",
        "key_bits": 256,
        "self_signed": false,
        "chain_status": "trusted",
        "chain_issuers": [
          "R3",
          "ISRG Root X1"
        ]
      }
    }
  ],
  "pagination": {
    "next_cursor": null,
    "limit": 100
  },
  "snapshot_time": "2026-07-30T00:00:00.000Z"
}

Errors

invalid_domain (400) · unauthorized (401) · domain_not_available (404) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503) · plan_required (403) · invalid_request (400) · invalid_cursor (400)

GET/v1/domains/zones/added

The domains that appeared in a TLD zone on a given day, with their nameservers. Use it to watch for newly registered domains — for example, to catch look-alikes of your brand as soon as they show up. Returns one day per call (default: the most recent available day); page through the results with the cursor, and filter by `tld` or by a substring of the domain name. Pass `pagination.next_cursor` back unchanged (with the same `date` and filters) to fetch the next page; `next_cursor: null` means you have reached the last page. Requires a paid plan.

Parameters

NameInRequiredDescription
datequerynoThe day to fetch (YYYY-MM-DD, UTC). Defaults to the most recent available day. A day with no data returns 404.
tldquerynoRestrict to these TLDs (repeatable or comma-separated), lowercased, no leading dot. Max 50. Omit for all TLDs.
limitquerynoResults per page (1–1000, default 100). A value above 1000 is clamped to 1000.
cursorquerynoOpaque cursor from a prior response's `pagination.next_cursor`. Pass it back unchanged, with the same path and query parameters, to fetch the next page. A cursor used with a different query returns 400 `invalid_cursor`.
containsquerynoOnly return domains whose name contains these substrings. Repeatable, up to 5 terms, each at least 3 characters; a domain must contain all of them.

Example response

{
  "date": "2026-08-29",
  "event": "added",
  "data": [
    {
      "domain": "example-brand-login.com",
      "tld": "com",
      "zone_date": "2026-08-29",
      "nameservers": [
        "ns1.registrar-dns.com",
        "ns2.registrar-dns.com"
      ]
    }
  ],
  "pagination": {
    "next_cursor": "v1.eyJrIjpbImNvbSIsImV4YW1wbGUuY29tIl0sImZwIjoiYTFiMiJ9",
    "limit": 100
  },
  "filters": {
    "tld": [
      "com"
    ],
    "contains": [
      "login"
    ]
  },
  "as_of": "2026-08-30T06:12:44.000Z",
  "latest_available": "2026-08-29"
}

Errors

unauthorized (401) · plan_required (403) · not_found (404) · invalid_request (400) · invalid_cursor (400) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503)

GET/v1/domains/zones/changed

The domains whose nameservers changed in a TLD zone on a given day, with both the previous and the new nameservers. Use it to spot registrar or hosting migrations — for example, to detect when a domain you track moves to a different provider. Returns one day per call (default: the most recent available day); page through the results with the cursor, and filter by `tld` or by a substring of the domain name. Pass `pagination.next_cursor` back unchanged (with the same `date` and filters) to fetch the next page; `next_cursor: null` means you have reached the last page. Requires a paid plan.

Parameters

NameInRequiredDescription
datequerynoThe day to fetch (YYYY-MM-DD, UTC). Defaults to the most recent available day. A day with no data returns 404.
tldquerynoRestrict to these TLDs (repeatable or comma-separated), lowercased, no leading dot. Max 50. Omit for all TLDs.
limitquerynoResults per page (1–1000, default 100). A value above 1000 is clamped to 1000.
cursorquerynoOpaque cursor from a prior response's `pagination.next_cursor`. Pass it back unchanged, with the same path and query parameters, to fetch the next page. A cursor used with a different query returns 400 `invalid_cursor`.
containsquerynoOnly return domains whose name contains these substrings. Repeatable, up to 5 terms, each at least 3 characters; a domain must contain all of them.

Example response

{
  "date": "2026-08-29",
  "event": "ns_changed",
  "data": [
    {
      "domain": "example.com",
      "tld": "com",
      "zone_date": "2026-08-29",
      "old_nameservers": [
        "ns1.old-host.com",
        "ns2.old-host.com"
      ],
      "new_nameservers": [
        "ns1.new-host.com",
        "ns2.new-host.com"
      ]
    }
  ],
  "pagination": {
    "next_cursor": null,
    "limit": 100
  },
  "filters": {
    "tld": null,
    "contains": null
  },
  "as_of": "2026-08-30T06:12:44.000Z",
  "latest_available": "2026-08-29"
}

Errors

unauthorized (401) · plan_required (403) · not_found (404) · invalid_request (400) · invalid_cursor (400) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503)

GET/v1/domains/zones/removed

The domains that were removed from a TLD zone on a given day. Use it to find dropped or expired domains — for example, to track when a name you want becomes available. Returns one day per call (default: the most recent available day); page through the results with the cursor, and filter by `tld` or by a substring of the domain name. Pass `pagination.next_cursor` back unchanged (with the same `date` and filters) to fetch the next page; `next_cursor: null` means you have reached the last page. Requires a paid plan.

Parameters

NameInRequiredDescription
datequerynoThe day to fetch (YYYY-MM-DD, UTC). Defaults to the most recent available day. A day with no data returns 404.
tldquerynoRestrict to these TLDs (repeatable or comma-separated), lowercased, no leading dot. Max 50. Omit for all TLDs.
limitquerynoResults per page (1–1000, default 100). A value above 1000 is clamped to 1000.
cursorquerynoOpaque cursor from a prior response's `pagination.next_cursor`. Pass it back unchanged, with the same path and query parameters, to fetch the next page. A cursor used with a different query returns 400 `invalid_cursor`.
containsquerynoOnly return domains whose name contains these substrings. Repeatable, up to 5 terms, each at least 3 characters; a domain must contain all of them.

Example response

{
  "date": "2026-08-29",
  "event": "removed",
  "data": [
    {
      "domain": "expired-domain.com",
      "tld": "com",
      "zone_date": "2026-08-29"
    }
  ],
  "pagination": {
    "next_cursor": null,
    "limit": 100
  },
  "filters": {
    "tld": null,
    "contains": null
  },
  "as_of": "2026-08-30T06:12:44.000Z",
  "latest_available": "2026-08-29"
}

Errors

unauthorized (401) · plan_required (403) · not_found (404) · invalid_request (400) · invalid_cursor (400) · rate_limited (429) · quota_exceeded (429) · service_unavailable (503)

Rate-limit headers

Every response carries X-RateLimit-Limit (your monthly request quota) and X-RateLimit-Remaining (requests left this month). A 429 additionally carries Retry-After (seconds to wait) — one second for a per-second rate limit, or seconds-until-reset for an exhausted daily/monthly quota.

Error codes

Errors are application/problem+json (RFC 7807): { "type", "title", "status", "detail" }. The type is the stable machine-readable code.

typeHTTPMeaning
invalid_domain400The domain could not be parsed.
invalid_request400A query parameter (date, tld, limit, or contains) was malformed.
invalid_cursor400The pagination cursor is malformed or does not belong to this query.
unauthorized401Missing, malformed, invalid, or revoked API key.
plan_required403The requested capability (e.g. live=true or a change timeline) is not on your plan.
not_found404No record/observation exists for this domain yet.
domain_not_available404No record is available for this domain.
rate_limited429Too many requests per second for your plan. Retry shortly.
quota_exceeded429A daily or monthly quota is exhausted. See Retry-After.
upstream_unavailable502The upstream WHOIS/RDAP lookup could not be completed.
service_unavailable503The API is temporarily unable to meter the request. Retry shortly.

A note on DNS & certificate coverage

Our DNS and certificate observations grow from interest: the record for a domain begins the first time someone looks it up — a first query for an unobserved domain is answered live and recorded, so current-state lookups always work. Change timelines build from that point on, and an early query for a rarely-seen domain may return a short (or empty) one. Coverage compounds from usage, including your own.

Changelog

  • v1.0 — Launch: WHOIS, RDAP, domain history summary, and DNS & certificate current/changes endpoints.