Skip to content
Inspect My DNS

API

DNS, mail and hosting reports as JSON. The same engine as the web interface, versioned under /api/v1 — a keyed request and a browser request measure exactly the same things and cost exactly the same.

Start here

No key, no sign-up, no preamble. One request returns the whole report:

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

It answers when the scan is finished. There is no job to poll and no pagination — the response is the report.

Every endpoint is also described in the OpenAPI 3.1 spec, which is the machine-readable version of this page.

Authentication

Optional. Anonymous access works at the same limits a browser gets, which is enough for interactive use and for a small monitoring job.

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

A key does two things: it raises the hourly ceiling, and it makes the rate-limit identity the key rather than your IP address — which matters when your team shares an outbound address, or when you run from a CI runner whose address you do not control.

TierRequests/hour
anonymous30
free100
standard1,000
high10,000

Create a key with an email address — no password. Only sha256(key) is stored, so a key is shown once at creation and cannot be recovered afterwards; losing it means creating a new one.

Endpoints

GET/api/v1/domain/:name

The report. Served from cache when a scan completed within the last 5 minutes, otherwise scanned fresh.

curl -s https://staging-v1.inspectmydns.com/api/v1/domain/example.com | jq '{score, scoreStatus, cached, counts}'
{
  "score": 91,
  "scoreStatus": null,
  "cached": true,
  "counts": { "pass": 30, "info": 6, "warn": 2, "fail": 0, "unknown": 1 }
}

Concurrent requests for the same domain wait on the scan already running rather than starting a second one, so repeated calls do not multiply the load on that domain’s nameservers.

POST/api/v1/domain/:name/refresh

Force a fresh scan, ignoring the cache.

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

GET/api/v1/domain/:name/history

Scans where the significant records actually changed, newest first.

curl -s https://staging-v1.inspectmydns.com/api/v1/domain/example.com/history \
  | jq '.entries[] | {scannedAt, changes}'
{
  "scannedAt": "2026-03-02T09:14:22.000Z",
  "changes": ["MX moved from Google Workspace to Microsoft 365"]
}

GET/api/v1/scan/:shareId

One specific historical scan, immutable.

The response

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

{
  "domain": "example.com",
  "scannedAt": "2026-08-14T15:36:00.000Z",
  "cached": false,
  "ageSeconds": 0,
  "shareUrl": "https://staging-v1.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": "…", "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 with the reasoning behind each.

Four 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. A low score is a real measurement; null means there was nothing to measure — do not render it as 0.
  • 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 it does not affect score. Treating it as a failure in your alerting will page you about our network, not yours.
  • reputation.partial: true means not measured. At least one blacklist refused to answer — Spamhaus and others reject queries arriving via public resolvers and enforce daily quotas. With it set, listed: false means we do not know, not clean.
  • cached: true comes with ageSeconds. If a record was 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 on a salted daily hash of your network block — the raw address is never stored.

CounterLimitWindowKeyed on
Fresh checks per domain315 minutesyou
Fresh checks across all domains30 anonymous, or your tier60 minutesyou
Fresh scans of one domain12060 minutesthe domain

Checking many different domains is normal use and the first counter does not touch it — it exists to catch repeated checks of one domain. The third is not about you at all, and no key raises it: scanning means issuing DNS queries and probing mail servers that belong to somebody who did not ask us to, and that ceiling holds however the requests are spread across callers.

Cached responses consume nothing. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and a 429 names which counter tripped in its scope field — so you know whether to back off on one domain, back off entirely, or simply wait because the domain is busy and it was never your budget. It includes the most recent stored report either way, under stale.

Errors

StatuserrorMeaning
400invalid_domainNot a plausible registrable name
403domain_blockedThe domain’s owner opted out of being scanned
404not_foundNo scan with that share id
429rate_limitedSee the scope field
502scan_failedThe scan itself errored — ours, not the domain’s

Using it from an agent

One request returns one complete report — no job to poll, no pagination — so this is a straightforward API for an agent to drive. What an agent needs telling is the shape of the answer rather than its format: that a severity: "unknown" is our measurement failing and not the domain’s, that a scan sends live queries to somebody else’s nameservers, and that a 429 already carries the report it is being told to wait for.

Paste this into a system prompt, a CLAUDE.md, a skill, or the top of a task description. It works with or without a key.

Getting a key

Not required, and worth having if you are running more than a handful of checks an hour or calling from an address you share with other people. Create one at /keys.