# Inspect My DNS API

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

Base URL: `https://inspectmydns.com/api/v1`
OpenAPI 3.1: https://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://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://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://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://inspectmydns.com/api/v1/domain/example.com/refresh
```

Add `?unlisted=1` to keep the result off the home page's recent list and out
of the sitemap. That is a publication choice, not privacy: the report stays
readable by anyone holding its URL.

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

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

```bash
curl -s https://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://inspectmydns.com/s/kWpmpKnwcPY3",
  "score": 91,
  "counts": { "pass": 30, "info": 6, "warn": 2, "fail": 0, "unknown": 1 },
  "providers": [
    { "provider": "AWS Route 53", "category": "dns", "confidence": "high" }
  ],
  "registrar": { "name": "…", "abuseEmail": "…", "statuses": [] },
  "reputation": { "enabled": true, "listed": false, "partial": false },
  "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://inspectmydns.com/checks.

### Three fields worth reading carefully

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

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.

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.
- If the user does not want the result advertised on the site, use
  POST /domain/{name}/refresh?unlisted=1. That keeps it off the home page's
  recent list and out of the sitemap. It is not privacy: anyone with the URL
  can still read the report.

How to read the response:

- `score` is 0-100. `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.
- `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
```
