> For the complete documentation index see https://domscan.net/llms.txt. Markdown versions of every docs page are available by appending .md to the page URL.

Source: https://domscan.net/docs/response-format

# Response format

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

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

```bash
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.

```bash
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.

| Parameter | Endpoints | Same as |
| --- | --- | --- |
| `prefer_cache` | `/v1/status`, `/v1/subdomains` | `max_age=2592000` |
| `skip_cache` | `/v1/status`, `/v1/tech`, `/v1/categorize`, `/v1/identity-assets`, `/v1/identity-resolution`, `/v1/url-intelligence`, `/v1/agent-readiness`, `/v1/robots` | `max_age=0` |
| `cache_skip` | `/v1/status` | `max_age=0` |
| `cache-skip` | `/v1/status` | `max_age=0` |
| `options.prefer_cache (body)` | `/v1/status/bulk` | `max_age=2592000` |
| `prefer_cache (body)` | `/v1/status/bulk` | `max_age=2592000` |
| `options.skip_cache (body)` | `/v1/status/bulk` | `max_age=0` |
| `options.cache_skip (body)` | `/v1/status/bulk` | `max_age=0` |
| `options.cache-skip (body)` | `/v1/status/bulk` | `max_age=0` |
| `refresh` | `/v1/ssl/deep-scan` | `max_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`.

| Endpoint | Default page size | Maximum limit |
| --- | --- | --- |
| `GET /v1/whois/history` | 50 | 100 |
| `GET /v1/dns/history` | 100 | 1000 |
| `GET /v1/subdomains` | 500 | 2000 |
| `GET /v1/certificates` | 100 | 1000 |
| `GET /v1/watchlist` | Whole list | 100 |
| `GET /v1/watchlist/expiring` | Whole list | 100 |
| `GET /v1/watchlist/available` | Whole list | 100 |
| `GET /v1/brand-monitor` | Whole list | 100 |
| `GET /v1/email/blacklist` | 1000 | 10000 |

#### Example

```bash
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.

```json
{
  "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.
