who.is API

Parsed WHOIS, normalized RDAP, and DNS and TLS certificate history for any domain — JSON over HTTPS. Start free, no card required.

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

PlanRequests / monthRequests / dayRequests / secondFull-history / monthLive refreshes / day
Free500100100
Pro API Access15,000No daily cap55250

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 Pro API Access — 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/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

The zone's change-only DNS timeline from our store-on-change ledger. An empty timeline is a valid 200 with an empty `changes` array — the coverage fields report how far back our observations go. Requires Pro API Access: on the Free plan this endpoint returns `plan_required` (403).

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",
  "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"
        ]
      }
    }
  ],
  "observed_since": "2024-06-01T00:00:00.000Z",
  "observation_count": 2,
  "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)

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

The host's certificate change timeline from our store-on-change ledger. An empty timeline is a valid 200 with an empty `changes` array — the coverage fields report observation depth. Requires Pro API Access: on the Free plan this endpoint returns `plan_required` (403).

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",
  "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"
        ]
      }
    }
  ],
  "observed_since": "2026-02-01T00:00:00.000Z",
  "observation_count": 2,
  "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)

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.
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. The timeline responses report how far back our data goes via observed_since and observation_count — coverage compounds from usage, including your own.

Changelog

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