API REFERENCE · GET · STABLE

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.

01 · OVERVIEW

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`.
02 · PARAMETERS

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.
03 · RESPONSE

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`.
04 · EXAMPLE

Example response

Truncated but representative example for a real auto-scan against `microsoft.com`:

Sample response
{
  "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": []
    }
  }
}
05 · ERRORS

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.
06 · BEST PRACTICES

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.
07 · FAQ

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.