# Inspect My DNS API

> One JSON diagnostic report per domain: DNS delegation, DNSSEC, mail authentication, TLS, registration and provider attribution.

Base URL: `https://staging.inspectmydns.com/api/v1`
OpenAPI 3.1: https://staging.inspectmydns.com/api/v1/openapi.json

## Authentication

Optional. Anonymous access works at the same limits a browser gets.

```bash
curl -H "Authorization: Bearer imd_a1b2c3d4_…" \
  https://staging.inspectmydns.com/api/v1/domain/example.com
```

A key raises the hourly ceiling and makes the rate-limit identity the key
rather than your IP address, which matters when a team shares an outbound
address or you run from a CI runner. Create one at https://staging.inspectmydns.com/keys. Only
`sha256(key)` is stored, so a key is shown once and cannot be recovered.

| Tier | Requests/hour |
|---|---|
| anonymous | 30 |
| free | 100 |
| standard | 1,000 |
| high | 10,000 |

## Endpoints

### GET /api/v1/domain/{name}

The report. Served from cache when a scan completed within the last 5 minutes,
otherwise it scans and returns when the scan is done. There is no job to poll.

```bash
curl -s https://staging.inspectmydns.com/api/v1/domain/example.com
```

Concurrent requests for the same domain wait on the scan already running rather
than starting a second one.

### POST /api/v1/domain/{name}/refresh

Force a fresh scan, ignoring the cache.

```bash
curl -X POST -H "Authorization: Bearer $INSPECTMYDNS_KEY" \
  https://staging.inspectmydns.com/api/v1/domain/example.com/refresh
```

### GET /api/v1/domain/{name}/history

Scans where the significant records actually changed, newest first, each with a
summary of what moved.

Web addresses are compared by the network they sit in — the announced prefix and
origin AS — rather than by the address itself, so a load balancer handing out a
different member of one rotation on every query is not a change. A move to a
genuinely different network appears once a second consecutive scan has seen it,
which is one scan later than it happened; everything else (nameservers, MX, SPF,
DKIM, DMARC, DNSSEC, registrar) is recorded on first observation.

```bash
curl -s https://staging.inspectmydns.com/api/v1/domain/example.com/history
```

### GET /api/v1/scan/{shareId}

One specific historical scan, immutable.

## The response

Stable within `v1`. Fields may be added; none are removed or retyped.

```json
{
  "domain": "example.com",
  "scannedAt": "2026-08-14T15:36:00.000Z",
  "cached": false,
  "ageSeconds": 0,
  "shareUrl": "https://staging.inspectmydns.com/s/kWpmpKnwcPY3",
  "score": 91,
  "scoreStatus": null,
  "counts": { "pass": 30, "info": 6, "warn": 2, "fail": 0, "unknown": 1 },
  "providers": [
    { "provider": "AWS Route 53", "category": "dns", "confidence": "high" }
  ],
  "registrar": { "name": "…", "url": "…", "abuseEmail": "…", "statuses": [] },
  "reputation": { "enabled": true, "listed": false, "partial": false },
  "safeBrowsing": { "checked": true, "listed": false, "error": null, "urls": ["https://example.com/"] },
  "checks": [
    {
      "id": "mailauth.spf-lookups",
      "category": "mail-auth",
      "title": "SPF stays within the 10-lookup limit",
      "severity": "warn",
      "summary": "SPF costs 9 of the 10 permitted DNS lookups.",
      "why": "Past 10 lookups receivers return permerror and SPF stops working."
    }
  ]
}
```

Every check has a stable `id` that is never renamed, so you can alert on
specific ones. All of them are listed at https://staging.inspectmydns.com/checks.

### Five fields worth reading carefully

- **`score` can be `null`.** That means the domain was never meaningfully
  present in DNS — no registry answer, no delegation, or no nameserver that
  responded at all — and `scoreStatus` names why (`"not_delegated"`,
  `"registry_unreachable"` or `"no_nameservers_responded"`). `counts` is
  still populated. Do not render a null score as 0 — a low score is a real
  measurement, and null means there was nothing to measure.
- **`severity: "unknown"` is not a failure.** It means *we* could not measure
  the check — most often because outbound port 25 is blocked on the machine
  running the scan. It says nothing about the domain and does not affect
  `score`. Treating it as a failure in alerting pages you about our network.
- **`reputation.partial: true` means not measured.** At least one blacklist
  refused to answer. With it set, `listed: false` means *we do not know*, not
  *clean*.
- **`safeBrowsing.checked: false` means nothing was asked.** Either the
  deployment has no Safe Browsing key or the scan predates the lookup. The API
  answers with an empty result for a clean URL, so `listed: false` without
  `checked: true` and `error: null` is *we do not know*, not *clean* — the
  same trap as `reputation.partial`. `urls` names what was submitted: the
  lists are per-URL, so a clean result covers those and not every page.
- **`cached: true` comes with `ageSeconds`.** If a record changed a minute
  ago, that number is why the change is not visible yet. It is a reason to
  wait, not a reason to call `/refresh` in a loop.

## Rate limits

Three counters. The first two are yours, keyed on a key id or a salted daily
hash of your network block — the raw address is never stored.

| Counter | Limit | Window | Keyed on |
|---|---|---|---|
| Fresh checks per domain | 3 | 15 minutes | you |
| Fresh checks across all domains | 30 anonymous, or your tier | 60 minutes | you |
| Fresh scans of one domain | 120 | 60 minutes | the domain |
| Read requests (`/scan/:id`, `/domain/:name/history`) | 60 anonymous, or your tier per minute | 1 minute | you |

Checking many different domains is normal use and the first counter does not
touch it. The third is not about you at all and no key raises it: scanning
means probing infrastructure belonging to somebody who did not ask us to. The
fourth bounds the per-request work of the read endpoints (a database read and,
for history, a diff) rather than a scarce action, so a key does not exempt you
from it — but it never falls below the per-minute share of your tier's hourly
scan ceiling.

Cached responses consume nothing. Every response carries
`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. A
`429` names which counter tripped in its `scope` field and includes the most
recent stored report under `stale` — use that rather than retrying.

## Errors

| Status | error | Meaning |
|---|---|---|
| 400 | invalid_domain | Not a plausible registrable name |
| 403 | domain_blocked | The domain's owner opted out of being scanned |
| 404 | not_found | No scan with that share id |
| 429 | rate_limited | See the `scope` field |
| 502 | scan_failed | The scan itself errored — ours, not the domain's |

## Using this API from an agent

Paste the following into a system prompt, a `CLAUDE.md`, a skill, or the top
of a task description.

```text
You can use Inspect My DNS (https://inspectmydns.com) to get one JSON
diagnostic report per domain: delegation and nameserver agreement, DNSSEC,
mail authentication (SPF, DKIM, DMARC, MTA-STS), mail and web hosts, TLS,
registration and expiry, and provider attribution.

Base URL: https://inspectmydns.com/api/v1
Auth: optional. With a key, send `Authorization: Bearer imd_…`. Without one you
get 30 fresh checks an hour, which is enough for interactive work.

Endpoints:
  GET  /domain/{name}           The report. Served from cache if a scan
                                finished within the last 5 minutes, otherwise
                                it scans and returns when the scan is done.
  POST /domain/{name}/refresh   Force a fresh scan.
  GET  /domain/{name}/history   Scans where the records actually changed,
                                newest first, each with a summary of what moved.
  GET  /scan/{shareId}          One immutable historical scan.

How to use it well:

- One GET per domain answers almost every question. Start there.
- Do not call /refresh unless the user is actively changing DNS and wants to
  watch the change land. A scan sends live queries to nameservers and mail
  servers belonging to somebody who did not ask to be probed; the plain GET
  reuses a recent one and costs them nothing.
- Do not poll and do not retry on a timeout. The GET returns when the scan is
  finished, and concurrent callers join the same scan rather than starting a
  second — there is no job id and nothing to wait on.
- Only look up domains the user actually asked about. Do not enumerate
  subdomains or scan every name you find in a page.

How to read the response:

- `score` is 0-100, or `null` when the domain was never meaningfully present
  in DNS — check `scoreStatus` for why, and do not report a null score as 0.
  `counts` breaks findings down by severity. `checks[]` is the detail: each
  has a stable `id` you can rely on, a `severity`, a one-line `summary`, and
  longer `detail` and `why` fields. Quote `summary` by default.
- `severity: "unknown"` means WE could not measure that check — most often a
  blocked outbound port on the machine running the scan. It says nothing about
  the domain and it is excluded from `score`. Never report it as a failure or
  alert on it.
- `reputation.partial: true` means a blacklist declined to answer. With it set,
  `listed: false` means "not measured", not "clean". Say which you mean.
- `safeBrowsing.checked: false` means no Safe Browsing lookup was made — no key,
  or a scan older than the check. `listed: false` is only clean alongside
  `checked: true` and `error: null`; the same distinction as above.
- `replayed: true` means the answers came from recorded fixtures rather than
  live DNS. Say so if you ever see it; it is always false in production.
- `cached: true` with `ageSeconds` tells you how old the report is. If the user
  just changed a record, an age of 200 seconds explains why it is not visible
  yet — that is not a reason to call /refresh in a loop.
- Link `shareUrl` when you report back, so the person can open the same report.

Errors:

- 400 invalid_domain — the name is not a plausible registrable domain.
- 403 domain_blocked — the domain's owner opted out of being scanned. Do not
  work around it.
- 404 not_found — no scan on record. Usually a share id that does not exist.
- 429 rate_limited — read `scope`. `domain` means back off this one domain,
  `global` means back off entirely, `target` means the domain is busy and it
  was never your budget. Honour `Retry-After`, and use the report in the
  `stale` field of the body instead of retrying — it is minutes old at worst.
- 502 scan_failed — the scan errored on our side, not the domain's. Report it
  as our failure, not as a finding against the domain.

Full documentation: https://inspectmydns.com/api/v1/openapi.json
```
