Response format

All responses are JSON. Each domain result contains these common fields:

  • domain - Complete domain name
  • available - Boolean availability status
  • source - "rdap" or "cache"
  • checked_at - ISO 8601 timestamp
  • error - Present if an error occurred

Response metadata

Each API response includes a request ID and freshness value. When available, response headers also show credits requested, charged, refunded, and remaining, plus your rate-limit plan, limit, remaining capacity, and policy. A freshness value of unknown means freshness could not be established for that response. It does not mean the data is current.

Field selection and compact responses

Add fields or compact to the query string of any GET or POST request to receive a smaller JSON response. Both are optional. On POST requests they go in the query string, not the body. Neither changes the credit cost.

Select fields

fields takes comma-separated dot paths, such as fields=domain,classification.status,records.data. When a path crosses a list, it applies to every item. Items keep their position, and an item without any selected field becomes {}. A null value along a path is kept as null.

Send up to 50 paths, each with up to 6 segments of 1 to 64 letters, digits or underscores. Any other value returns 400 INVALID_PARAMETER before the request runs, so nothing is charged.

Paths that match nothing in the response are left out and listed in the X-Fields-Ignored response header, up to 20 paths.

Compact responses

compact=true removes only the following, and only at the top level of the response:

  • In the meta object: served_by and the timing keys duration_ms, query_time_ms, total_ms, total_time_ms, latency_ms, generation_ms and check_duration_ms. The meta object itself is removed when nothing else is left in it.
  • warnings, caveats, notes and errors when they are empty lists.
  • request_id when it repeats the X-Request-Id header.

Data fields, classification, nested objects and list items are never removed. When both parameters are sent, compact runs first and fields second.

Only successful JSON responses are reshaped. Error responses, CSV, text and streamed responses, and dry_run estimates are returned unchanged. Reshaped responses are sent without indentation.

With an Idempotency-Key, fields and compact are part of the request. A retry with the same values returns the stored, reshaped response. Reusing the key with different values returns 422 IDEMPOTENCY_KEY_REUSED.

MCP tools accept the same optional fields and compact arguments. Unmatched paths are listed in _meta.fields_ignored on the tool result.

Example

curl -H "X-API-Key: $DOMSCAN_API_KEY" "https://domscan.net/v1/dns?domain=google.com&type=TXT&fields=domain,status,records.data,records.ttl,owner&compact=true"

X-Fields-Ignored: owner

{
  "domain": "google.com",
  "records": [
    {
      "data": "v=spf1 include:_spf.google.com ~all",
      "ttl": 300
    },
    {
      "data": "google-site-verification=abc123",
      "ttl": 300
    }
  ],
  "status": "success"
}

snake_case keys (naming)

Most response keys are already snake_case, but a few endpoints return some camelCase keys, such as registrarName in pricing results, tldTier in valuation factors and eventDate in RDAP events. Add naming=snake to the query string, or send the DomScan-Naming: snake header, to receive every one of those keys in snake_case. Without it, responses keep their current keys.

A key changes only when it starts with a lowercase letter, contains only letters and digits and has at least one capital letter. Each new word gets an underscore and the key is lowercased. Acronyms stay together, including IPv4 and plurals such as IPs: registrarName becomes registrar_name, secureDNS becomes secure_dns, ipv4Address becomes ipv4_address and nameserverIPs becomes nameserver_ips.

Keys that are data rather than field names keep their exact spelling, for example DNS record types such as A and MX, grades such as A+, domain names, header names and technology names. Values are never changed, and nested objects and lists are converted at every level.

If an object already has the snake_case name, for example both fooBar and foo_bar, foo_bar keeps its value and fooBar keeps its original name, so nothing is lost. Key order does not change.

The query parameter wins over the header, so naming=default turns a header opt-in off for one request. Accepted values are snake and default. Any other value returns 400 INVALID_PARAMETER before the request runs, so nothing is charged. Responses include Vary: DomScan-Naming.

naming runs before compact and fields, so write fields paths in snake_case, such as fields=factors.tld_tier. Error responses are already snake_case and never change, and the credit cost is the same.

With an Idempotency-Key, the naming choice is part of the request whether it comes from the parameter or the header, so a retry returns the same keys.

MCP tools accept the same optional naming argument. MCP calls do not read the header.

curl -H "X-API-Key: $DOMSCAN_API_KEY" -H "DomScan-Naming: snake" "https://domscan.net/v1/value?domain=example.com&fields=domain,factors.tld_tier,factors.has_hyphens"

{
  "domain": "example.com",
  "factors": {
    "tld_tier": "premium",
    "has_hyphens": false
  }
}

Freshness control (max_age)

Endpoints that can answer from recently checked data accept max_age, a whole number of seconds from 0 to 2592000 (30 days). Data older than max_age is checked again before it is returned, and max_age=0 always checks fresh. Without max_age, each endpoint keeps its usual behavior.

Every successful response from these endpoints includes a top-level cache_age_s: the age in seconds of the data used, or 0 when it was checked fresh. The same value is sent in the X-Cache-Age response header. When a response holds several items, such as an availability check for several domains, it is the age of the oldest item, and items keep their own cache_age_s where they already had one.

max_age never changes how a result is classified. A result that could not be confirmed stays unknown however recent it is, and recent data is never shown as present or absent unless the original check confirmed it.

max_age does not change what a request costs. A fresh check and an answer from recent data cost the same credits.

On /v1/status and /v1/status/bulk, forced fresh checks (max_age=0) are limited per client to protect registries. Beyond that limit the request is answered as if max_age were not sent, and cache_age_s shows the age of what you received. A max_age that is not a whole number from 0 to 2592000 returns 400 INVALID_PARAMETER before the request runs, so nothing is charged.

Endpoints that accept max_age: GET /v1/status, POST /v1/status/bulk, GET /v1/subdomains, GET /v1/certificates, GET /v1/tech, GET /v1/categorize, GET /v1/identity-assets, GET /v1/identity-resolution, GET /v1/url-intelligence, GET /v1/agent-readiness, GET /v1/robots, GET /v1/ssl/deep-scan

Older cache parameters

These parameters keep working and are now deprecated aliases of max_age. When you also send max_age, max_age wins.

ParameterEndpointsSame as
prefer_cache/v1/status, /v1/subdomainsmax_age=2592000
skip_cache/v1/status, /v1/tech, /v1/categorize, /v1/identity-assets, /v1/identity-resolution, /v1/url-intelligence, /v1/agent-readiness, /v1/robotsmax_age=0
cache_skip/v1/statusmax_age=0
cache-skip/v1/statusmax_age=0
options.prefer_cache (body)/v1/status/bulkmax_age=2592000
prefer_cache (body)/v1/status/bulkmax_age=2592000
options.skip_cache (body)/v1/status/bulkmax_age=0
options.cache_skip (body)/v1/status/bulkmax_age=0
options.cache-skip (body)/v1/status/bulkmax_age=0
refresh/v1/ssl/deep-scanmax_age=0

On /v1/subdomains, prefer_cache also keeps its cache-only behavior: when nothing is stored yet, the request returns 202 without charging and starts the lookup in the background.

MCP tools for these endpoints accept the same optional max_age argument, and their results include cache_age_s.

Pagination (limit and cursor)

Endpoints that return long lists accept limit and cursor, and every response includes has_more and next_cursor. To get the next page, repeat the request with cursor set to next_cursor. On the last page has_more is false and next_cursor is null.

Without limit, each endpoint returns the same number of items as before, so existing requests are unchanged. You can change limit from one page to the next.

Cursors are opaque and signed. A cursor only works for the account that received it and for the same query, such as the same domain and filters. A cursor that was edited, belongs to another account or is sent with different filters returns 400 INVALID_CURSOR.

Pages follow a stable order, so items added or removed between requests are not repeated or skipped. On the history endpoints, summary fields describe the items on the current page.

Each page is a normal request and costs the endpoint's usual credits, exactly as a request with limit did before. Free endpoints stay free.

Older parameters keep working: offset on /v1/email/blacklist, and the numeric pagination.next_cursor on /v1/certificates.

EndpointDefault page sizeMaximum limit
GET /v1/whois/history50100
GET /v1/dns/history1001000
GET /v1/subdomains5002000
GET /v1/certificates1001000
GET /v1/watchlistWhole list100
GET /v1/watchlist/expiringWhole list100
GET /v1/watchlist/availableWhole list100
GET /v1/brand-monitorWhole list100
GET /v1/email/blacklist100010000

Example

curl -H "X-API-Key: $DOMSCAN_API_KEY" "https://domscan.net/v1/whois/history?domain=example.com&limit=20"

{
  "domain": "example.com",
  "snapshots": [
    "..."
  ],
  "has_more": true,
  "next_cursor": "eyJvIjoi..."
}

curl -H "X-API-Key: $DOMSCAN_API_KEY" "https://domscan.net/v1/whois/history?domain=example.com&limit=20&cursor=eyJvIjoi..."

Classification status

Endpoints that report whether something exists (domain registration, social handles, DNS and email authentication records, security.txt, robots.txt, sitemaps, security headers, parking, typosquat registrations and SSL certificates) add a classification object next to their existing fields. Existing fields keep their names and meaning.

  • status - present, absent or unknown. Present and absent are returned only when the check produced exact, authoritative evidence.
  • reason - Returned only when the status is unknown. Explains why no definite answer was possible.
  • evidence - Where the answer came from: source, method, observed_at and an optional detail such as http_404 or nxdomain.
  • checked_at - When DomScan produced the classification. For cached results, evidence.observed_at is earlier.

Unknown reasons

  • blocked - The target refused access, for example with HTTP 401, 403 or a bot challenge.
  • rate_limited - The target or registry asked us to slow down, for example with HTTP 429.
  • timeout - The check did not finish within its time budget.
  • ambiguous - A response arrived but did not prove presence or absence, for example an unexpected status code or a malformed record.
  • upstream_error - The data source failed, for example a DNS server failure or an HTTP 5xx response.
  • unsupported - This check is not available for the input, for example an unsupported TLD or a platform without a reliable checker.
{
  "classification": {
    "status": "unknown",
    "reason": "rate_limited",
    "evidence": {
      "source": "rdap",
      "method": "rdap_lookup",
      "observed_at": "2026-10-10T09:30:00.000Z",
      "detail": "http_429"
    },
    "checked_at": "2026-10-10T09:30:00.000Z"
  }
}
A blocked, rate-limited, timed-out or failed check is never reported as absent. Treat unknown as "try again later" rather than "not there", and never read absence as proof that a name can be claimed.

Scores and grades with unknown checks

Scores never count an unknown check as missing. The DNS security score and the email authentication grade leave unknown checks out, report them in checks_unknown and set score_confidence to partial. When every check is unknown, the score and grade are null. The SSL grade and score are null with grade_reason set to certificate_not_retrieved when no certificate could be read, and the reputation score reports unknown parking as null instead of not parked.