DKIM Key API — /api/v1/domain/dkim
Auto-scans ~40 well-known ESP selectors in parallel (or resolves a single selector on request), parses each public key via ASN.1 DER to report the true modulus bit-length, detects the ESP from the selector name + CNAME target, aggregates severity-tagged validation checks, computes an A–F grade, and (async) returns an AI diagnosis with copy-paste key-rotation snippets.
What DKIM Key does
Two modes: auto-scan (no selector supplied) fans out common ESP selectors in parallel; explicit-selector mode queries the exact selector. Both return the same schema so callers only handle one response shape.
- Method GET
- Path /api/v1/domain/dkim
- Stability STABLE
- Version v2026-07-16
- Summary Multi-selector DKIM scan with provider detection, ASN.1 modulus math, and AI diagnosis.
- AI analysis Returned inline when cached (`X-Analysis-Status: complete`); otherwise `AiAnalysisMeta.id` is returned and the client polls `/api/v1/ai/analysis/status/:id`.
Request parameters
Pass these as query-string parameters. `domain` is the only required field.
- domain (required) — string Apex or subdomain. Normalized via public-suffix parsing before lookup.
- selector — string (optional) Explicit selector to query. When omitted (or when set to the legacy `default` placeholder), the backend runs auto-scan mode across ~40 common ESP selectors. When supplied, only that selector is queried.
- selectors — string | string[] (optional) CSV or repeated. Hint selectors to add to the scan list — typically populated from a pasted `DKIM-Signature: s=` value. Merged with the built-in common-selector list.
- refresh — boolean, default false Bypass the AI-analysis cache and force a fresh Gemini evaluation. Costs a fresh model call — use sparingly.
Response schema
All successful responses follow the standard `{ success: true, data: {...} }` envelope. Notable fields on the data object:
- data.found — boolean True when at least one selector published a valid `v=DKIM1` record.
- data.domain — string Normalized apex domain.
- data.mode — string `scan` for auto-scan mode, `selector` when an explicit selector was supplied.
- data.requestedSelector — string | null The single selector requested, or null in scan mode.
- data.selectors — array<object> One entry per selector that resolved. See per-selector schema below.
- data.selectorsNotFound — array<object> Only populated in explicit-selector mode when the requested selector didn't resolve.
- data.triedSelectors — array<string> All selectors we probed, whether they resolved or not.
- data.triedCount — number Total number of parallel probes issued.
- data.score — object `{grade: 'A'|'B'|'C'|'D'|'F', label, selectorCount, weakestKeyBits}`. Domain grade = worst per-selector grade.
- data.tests — array<object> Aggregate validation checks: `{test, result, passed, severity: 'high'|'med'|'low'}`. Five checks (present, modern strength, no testing, no revoked, provider identified).
- data.AiAnalysis — object | null Present when analysis is cached and complete. `{summary, quick_recommendations[], security_assessment[{title, description, severity}], fixes[{issue, kind: 'dns_txt'|'shell'|'dig', host, snippet}]}`. `null` when still being generated — poll `data.AiAnalysisMeta.id`.
- data.AiAnalysisMeta — object | null `{id, avgMs, etaMs}` — pointer used to poll `/api/v1/ai/analysis/status/:id`.
- Per-selector object `{selector, host, found, dkim, parsed, cnames, provider}`. See below.
- selectors[].selector — string Selector name (e.g. `google`, `selector1`, `k1`).
- selectors[].host — string Full DNS name queried: `<selector>._domainkey.<domain>`.
- selectors[].cnames — array<{from,to}> CNAME delegation chain, up to 5 hops. Empty for direct TXT records.
- selectors[].dkim — string Raw DKIM TXT value (multi-string chunks joined per RFC 6376 § 3.6.2.2).
- selectors[].parsed.keyLength — number | null Actual RSA modulus bit-length via ASN.1 DER parse. NOT the raw base64 byte-count.
- selectors[].parsed.keyType — string `rsa` or `ed25519`.
- selectors[].parsed.publicKey — string | null Raw base64 SPKI. `null` when the selector is revoked (empty `p=`).
- selectors[].parsed.revoked — boolean True when `p=` is present but empty (RFC 6376 revocation signal).
- selectors[].parsed.t — string | undefined Flags. `t=y` = testing mode; `t=s` = strict identity match.
- selectors[].provider — string Detected ESP name (e.g. `Google Workspace`, `Mailchimp / Mandrill`, `Microsoft 365`), or `Unknown / self-hosted`.
Example response
Truncated but representative example for a real auto-scan against `microsoft.com`:
{
"success": true,
"data": {
"found": true,
"domain": "microsoft.com",
"mode": "scan",
"requestedSelector": null,
"triedCount": 40,
"triedSelectors": ["default", "dkim", "selector1", "selector2", "k1", "..."],
"selectors": [
{
"selector": "selector1",
"host": "selector1._domainkey.microsoft.com",
"found": true,
"dkim": "v=DKIM1; k=rsa; p=MIIBIjANBg...",
"cnames": [
{ "from": "selector1._domainkey.microsoft.com", "to": "selector1-microsoft-com._domainkey.microsoft.onmicrosoft.com" }
],
"parsed": {
"valid": true,
"version": "DKIM1",
"keyType": "rsa",
"keyLength": 2048,
"keyStrength": "Strong",
"publicKey": "MIIBIjANBg...",
"publicKeyFingerprint": "b4c96aed72f5a831",
"warnings": [],
"errors": []
},
"provider": "Microsoft 365"
}
],
"selectorsNotFound": [],
"score": {
"grade": "A",
"label": "Strong",
"selectorCount": 1,
"weakestKeyBits": 2048
},
"tests": [
{ "test": "DKIM selector present", "passed": true, "severity": "high", "result": "1 selector publishes DKIM" },
{ "test": "Modern key strength (≥2048 bit)", "passed": true, "severity": "med", "result": "All selectors use ≥2048-bit keys" },
{ "test": "Not in testing mode", "passed": true, "severity": "low", "result": "No selectors in testing mode" },
{ "test": "No revoked selectors", "passed": true, "severity": "low", "result": "No revoked selectors" },
{ "test": "Sender provider identified", "passed": true, "severity": "low", "result": "Detected: Microsoft 365" }
],
"AiAnalysis": {
"summary": "microsoft.com signs mail with one 2048-bit selector delegated to Microsoft 365 tenant DNS — modern, correctly configured.",
"quick_recommendations": ["Publish a second selector as a rotation target", "Enforce DMARC at p=reject once alignment is verified"],
"security_assessment": [{ "title": "Modern key strength", "description": "2048-bit RSA — matches current best practice.", "severity": "info" }],
"fixes": []
}
}
}Error codes
Errors return HTTP status codes paired with stable machine-readable codes. Match on the code field rather than the human-readable message.
- 200 with data.found=false No selectors resolved — either the domain doesn't sign mail, or all selectors it uses are outside our scan list. Not an HTTP error; `success:true` with `found:false`.
- 400 INVALID_DOMAIN The `domain` parameter is missing or not a valid domain string.
- 500 UNEXPECTED_ERROR Upstream DNS resolver failure. Retry with backoff.
Implementation tips
Patterns we recommend when integrating this endpoint:
- Prefer auto-scan mode Omit `selector` unless you know the exact selector name. Auto-scan surfaces every ESP signing on your behalf including services onboarded without your knowledge.
- Read the grade, not just the tag `data.score.grade` is the number that matters for dashboards. Alerting on `parsed.valid` misses the case where the key is valid but 1024-bit (still deliverable, but a legacy risk).
- Alert on new selectors appearing Cron the endpoint per domain. Diff the returned selector list against your last snapshot. New selectors = new services signing as you; investigate before assuming they're legitimate.
- Handle testing-mode explicitly Check `selectors[].parsed.t === 'y'`. Testing mode is the second-most-common silent failure after using the wrong selector.
- Verify key rotation completion During rotation, the new selector shows up while the old one moves to `revoked: true`. If both are still `valid: true` after 60 days, retire the old one.
- Handle the async AI analysis When `AiAnalysis` is null and `AiAnalysisMeta.id` is present, poll `/api/v1/ai/analysis/status/:id` with the `avgMs` hint. Do not fail the response on missing AI — the structured fields are complete without it.
Frequently asked questions
- How do I find a domain's DKIM selectors? Two options: (1) omit `selector` and let auto-scan probe ~40 well-known ESP selectors; (2) parse the `s=` field from a real signed message's `DKIM-Signature:` header and pass it via `selectors=`. Selectors are not enumerable via DNS itself.
- How is the grade computed? A = every selector uses ≥2048-bit RSA (or Ed25519), no testing-mode, no revoked selectors. B = at least one 1024-bit key. C = at least one selector in testing mode. D = at least one revoked selector. F = no selectors resolved. Domain grade = worst per-selector grade.
- Why does the key length differ from what other tools report? Most tools measure the base64-decoded byte-length of the entire SPKI blob (~298 bytes for a 2048-bit RSA key, which they multiply by 8 to report ~2384 bits). We parse the ASN.1 DER structure to reach the modulus INTEGER and count its true bit-length — matches what `openssl rsa -pubin -text` reports.
- What's the difference between scan mode and selector mode? Same response schema, different lookup strategy. Scan mode fans out ~40 parallel probes with a 1.2s per-query cap (whole scan ~1.5s). Selector mode issues one lookup. Use scan mode for discovery; use selector mode for CI checks where you know the exact selector.