{"openapi":"3.1.0","info":{"title":"doesmyemail.work DNS Scan API","version":"1.0.0","description":"Public, free API that checks a domain's email authentication (SPF, DKIM, DMARC, MX) using public DNS. No authentication required. Read-only and synchronous. The single-domain GET endpoints are CORS-enabled (Access-Control-Allow-Origin: *), so they can be called directly from browser pages; the batch endpoint is same-origin only. Rate limited to 10 distinct domains per hour per IP; every response carries RateLimit-* headers (and Retry-After on 429). All error responses are JSON. Versioning: stable endpoints live under /api/v1. The preferred single-domain URL style is path-based: /api/v1/dns-scan/{domain}. The query-string form /api/v1/dns-scan?domain= remains supported; the unversioned /api/dns-scan is a backward-compatible alias of it. When a version is retired it will be announced in advance via the OpenAPI description and the changelog; new fields may be added to responses without a version bump.","contact":{"name":"Valentin Bora","email":"valentin@doesmyemail.work","url":"https://mach10.pro"},"license":{"name":"Free to use"}},"servers":[{"url":"https://doesmyemail.work"}],"x-sandbox":{"description":"Test environment: scan the domain \"sandbox.doesmyemail.work\" (e.g. GET /api/v1/dns-scan/sandbox.doesmyemail.work), or add the query param test=1, to receive a deterministic, fully-passing sample ScanResult without touching DNS or consuming rate limit. Sandbox responses carry the header X-Sandbox: 1.","domain":"sandbox.doesmyemail.work"},"paths":{"/api/v1/dns-scan/{domain}":{"get":{"operationId":"scanDomainByPath","summary":"Scan a domain's email authentication records (preferred URL style)","description":"Looks up SPF, DKIM (common selectors), DMARC, MX, and NS records for the domain and returns a structured assessment. Preferred URL style: the domain is part of the path, which keeps results distinct in URL-keyed caches; some fetchers and intermediaries discard query strings when building cache keys, which makes the ?domain= form return stale or wrong-domain results. Identical behavior to GET /api/v1/dns-scan?domain=. Pass test=1 (or use the domain sandbox.doesmyemail.work) to get deterministic sample data.","parameters":[{"name":"domain","in":"path","required":true,"description":"Bare domain to check, e.g. \"example.com\".","schema":{"type":"string","pattern":"^[a-z0-9.-]+\\.[a-z]{2,20}$"}},{"name":"test","in":"query","required":false,"description":"Set to \"1\" to return deterministic sandbox data (no DNS lookup, no rate-limit consumption).","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Scan result. Sandbox/test responses additionally carry X-Sandbox: 1.","headers":{"RateLimit-Limit":{"description":"Maximum number of distinct domains allowed per hour per IP.","schema":{"type":"string","example":"10"}},"RateLimit-Remaining":{"description":"Distinct-domain budget remaining in the current hourly window.","schema":{"type":"string","example":"7"}},"RateLimit-Reset":{"description":"Seconds until the current rate-limit window resets (top of the next hour).","schema":{"type":"string","example":"1834"}},"X-Sandbox":{"description":"Present and set to \"1\" only for sandbox/test responses.","schema":{"type":"string","example":"1"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanResult"}}}},"400":{"description":"Invalid domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"DNS did not resolve any records for the domain (likely misspelled or unregistered).","headers":{"RateLimit-Limit":{"description":"Maximum number of distinct domains allowed per hour per IP.","schema":{"type":"string","example":"10"}},"RateLimit-Remaining":{"description":"Distinct-domain budget remaining in the current hourly window.","schema":{"type":"string","example":"7"}},"RateLimit-Reset":{"description":"Seconds until the current rate-limit window resets (top of the next hour).","schema":{"type":"string","example":"1834"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (10 distinct domains/hour/IP).","headers":{"RateLimit-Limit":{"description":"Maximum number of distinct domains allowed per hour per IP.","schema":{"type":"string","example":"10"}},"RateLimit-Remaining":{"description":"Distinct-domain budget remaining in the current hourly window.","schema":{"type":"string","example":"7"}},"RateLimit-Reset":{"description":"Seconds until the current rate-limit window resets (top of the next hour).","schema":{"type":"string","example":"1834"}},"Retry-After":{"description":"Seconds to wait before retrying (until the top of the next hour).","schema":{"type":"string","example":"1834"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/dns-scan":{"get":{"operationId":"scanDomain","summary":"Scan a domain's email authentication records (query-string form)","description":"Looks up SPF, DKIM (common selectors), DMARC, MX, and NS records for the domain and returns a structured assessment. Secondary URL style: prefer GET /api/v1/dns-scan/{domain}, which survives caches that discard query strings. Pass test=1 (or use the domain sandbox.doesmyemail.work) to get deterministic sample data.","parameters":[{"name":"domain","in":"query","required":true,"description":"Bare domain to check, e.g. \"example.com\".","schema":{"type":"string","pattern":"^[a-z0-9.-]+\\.[a-z]{2,20}$"}},{"name":"test","in":"query","required":false,"description":"Set to \"1\" to return deterministic sandbox data (no DNS lookup, no rate-limit consumption).","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Scan result. Sandbox/test responses additionally carry X-Sandbox: 1.","headers":{"RateLimit-Limit":{"description":"Maximum number of distinct domains allowed per hour per IP.","schema":{"type":"string","example":"10"}},"RateLimit-Remaining":{"description":"Distinct-domain budget remaining in the current hourly window.","schema":{"type":"string","example":"7"}},"RateLimit-Reset":{"description":"Seconds until the current rate-limit window resets (top of the next hour).","schema":{"type":"string","example":"1834"}},"X-Sandbox":{"description":"Present and set to \"1\" only for sandbox/test responses.","schema":{"type":"string","example":"1"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanResult"}}}},"400":{"description":"Invalid domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"DNS did not resolve any records for the domain (likely misspelled or unregistered).","headers":{"RateLimit-Limit":{"description":"Maximum number of distinct domains allowed per hour per IP.","schema":{"type":"string","example":"10"}},"RateLimit-Remaining":{"description":"Distinct-domain budget remaining in the current hourly window.","schema":{"type":"string","example":"7"}},"RateLimit-Reset":{"description":"Seconds until the current rate-limit window resets (top of the next hour).","schema":{"type":"string","example":"1834"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (10 distinct domains/hour/IP).","headers":{"RateLimit-Limit":{"description":"Maximum number of distinct domains allowed per hour per IP.","schema":{"type":"string","example":"10"}},"RateLimit-Remaining":{"description":"Distinct-domain budget remaining in the current hourly window.","schema":{"type":"string","example":"7"}},"RateLimit-Reset":{"description":"Seconds until the current rate-limit window resets (top of the next hour).","schema":{"type":"string","example":"1834"}},"Retry-After":{"description":"Seconds to wait before retrying (until the top of the next hour).","schema":{"type":"string","example":"1834"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/dns-scan":{"get":{"operationId":"scanDomainUnversioned","deprecated":true,"summary":"Deprecated unversioned alias of /api/v1/dns-scan","description":"Backward-compatible alias of GET /api/v1/dns-scan with identical behavior. Retained for the existing frontend and early integrations. New integrations should call /api/v1/dns-scan.","parameters":[{"name":"domain","in":"query","required":true,"description":"Bare domain to check, e.g. \"example.com\".","schema":{"type":"string","pattern":"^[a-z0-9.-]+\\.[a-z]{2,20}$"}},{"name":"test","in":"query","required":false,"description":"Set to \"1\" to return deterministic sandbox data.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Scan result.","headers":{"RateLimit-Limit":{"description":"Maximum number of distinct domains allowed per hour per IP.","schema":{"type":"string","example":"10"}},"RateLimit-Remaining":{"description":"Distinct-domain budget remaining in the current hourly window.","schema":{"type":"string","example":"7"}},"RateLimit-Reset":{"description":"Seconds until the current rate-limit window resets (top of the next hour).","schema":{"type":"string","example":"1834"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanResult"}}}},"400":{"description":"Invalid domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"DNS did not resolve.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/scan":{"post":{"operationId":"scanBatch","summary":"Batch-scan multiple domains with pagination","description":"Scans up to 5 domains per request. Pass a domains array and an optional cursor; the response returns a page of results plus a pagination object. Repeat with nextCursor until hasMore is false. Each scanned domain counts against the per-IP hourly rate limit (sandbox domains do not).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"}}}},"responses":{"200":{"description":"A page of scan results plus pagination metadata.","headers":{"RateLimit-Limit":{"description":"Maximum number of distinct domains allowed per hour per IP.","schema":{"type":"string","example":"10"}},"RateLimit-Remaining":{"description":"Distinct-domain budget remaining in the current hourly window.","schema":{"type":"string","example":"7"}},"RateLimit-Reset":{"description":"Seconds until the current rate-limit window resets (top of the next hour).","schema":{"type":"string","example":"1834"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}},"400":{"description":"Missing/invalid JSON body or empty domains array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"405":{"description":"Wrong method (GET not allowed on this path).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"scanBatchMethodNotAllowed","summary":"Not allowed (use POST)","responses":{"405":{"description":"Use POST.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/ask":{"post":{"operationId":"ask","summary":"Natural-language query (NLWeb /ask)","description":"Accepts a natural-language query, extracts a domain or email address from it, scans that domain, and returns a plain-English summary. If no domain is found, returns guidance on how to phrase the question. Supports Server-Sent Events streaming when prefer.streaming is true or a \"prefer: streaming\" header is sent.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskRequest"}}}},"responses":{"200":{"description":"Summary answer (JSON) or a Server-Sent Events stream when streaming is requested.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskResponse"}},"text/event-stream":{"schema":{"type":"string","description":"SSE stream: a \"start\" event ({_meta}), one or more \"result\" events ({type:\"answer\", text, domain}), then a \"complete\" event."}}}},"400":{"description":"Missing/invalid JSON body or empty query.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"405":{"description":"Wrong method (GET not allowed on this path).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"ScanResult":{"type":"object","properties":{"domain":{"type":"string","description":"The domain that was scanned. Always verify this matches the domain you requested."},"spf":{"type":"object","properties":{"found":{"type":"boolean"},"value":{"type":["string","null"]},"lookups":{"type":["integer","null"],"description":"SPF DNS lookup count (limit is 10)."},"issues":{"type":"array","items":{"type":"string"},"description":"e.g. permissive_all, ptr_deprecated, multiple_records."},"mechanisms":{"type":"array","items":{"type":"string"}}},"required":["found","value","lookups","issues","mechanisms"]},"dkim":{"type":"object","properties":{"found":{"type":"boolean"},"selector":{"type":["string","null"]}},"required":["found","selector"]},"dmarc":{"type":"object","properties":{"found":{"type":"boolean"},"enforced":{"type":"boolean","description":"true when policy is quarantine or reject."},"value":{"type":["string","null"]}},"required":["found","enforced","value"]},"mx":{"type":"object","properties":{"found":{"type":"boolean","description":"true when any MX record exists in DNS (including null MX)."},"null_mx":{"type":"boolean","description":"true when the domain publishes RFC 7505 null MX (0 .), meaning it does not accept inbound mail."},"receives_mail":{"type":"boolean","description":"true when real mail exchanger hosts are configured."},"records":{"type":"array","items":{"type":"string"}},"provider":{"type":["string","null"]}},"required":["found","null_mx","receives_mail","records","provider"]},"cloudflare":{"type":"boolean"},"nameservers":{"type":"array","items":{"type":"string"}},"provider":{"type":"string","description":"Detected DNS/registrar provider."}},"required":["domain","spf","dkim","dmarc","mx","cloudflare","nameservers","provider"]},"Error":{"type":"object","properties":{"error":{"type":"string","description":"Machine-readable error code, e.g. dns-failed, rate-limited, invalid-domain."},"message":{"type":"string"},"hints":{"type":"array","items":{"type":"string"},"description":"Optional remediation hints."}},"required":["error"]},"BatchRequest":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"string","pattern":"^[a-z0-9.-]+\\.[a-z]{2,20}$"},"minItems":1,"description":"Domains to scan. The endpoint processes up to 5 per request starting at cursor."},"cursor":{"type":"integer","minimum":0,"default":0,"description":"Zero-based index into the domains array to start this page."}},"required":["domains"]},"BatchResponse":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string"},"result":{"$ref":"#/components/schemas/ScanResult"},"error":{"type":"string","description":"Present instead of result when this domain failed (invalid-domain, dns-failed, rate-limited)."},"message":{"type":"string"}},"required":["domain"]}},"pagination":{"type":"object","properties":{"cursor":{"type":"integer","description":"Cursor used for this page."},"pageSize":{"type":"integer","const":5},"nextCursor":{"type":["integer","null"],"description":"Cursor for the next page, or null when there are no more."},"hasMore":{"type":"boolean"},"total":{"type":"integer","description":"Total number of domains submitted."}},"required":["cursor","pageSize","nextCursor","hasMore","total"]}},"required":["results","pagination"]},"AskRequest":{"type":"object","properties":{"query":{"type":"string","description":"Natural-language question, e.g. \"is mail@example.com set up right?\"."},"prefer":{"type":"object","properties":{"streaming":{"type":"boolean","description":"Set true to receive a Server-Sent Events stream instead of a single JSON response."}}}},"required":["query"]},"AskResponse":{"type":"object","properties":{"_meta":{"type":"object","properties":{"response_type":{"type":"string","const":"summary"},"version":{"type":"string","const":"0.1"}}},"query":{"type":"string"},"results":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","const":"answer"},"text":{"type":"string"},"domain":{"type":["string","null"]}},"required":["type","text","domain"]}}},"required":["_meta","query","results"]}}}}