SPF Record API — /api/v1/domain/spf
Returns the raw SPF string, parsed mechanisms / qualifiers / directives, optional include expansion, RFC 7208 compliance checks (with `tests=true`), and — when analysis is cached — an AI review with severity-ranked issues plus quick recommendations. Client-side helpers (`spf-analytics.js`) derive policy strength, health score, lookup / void counts, sender inventory, provider fingerprint and a sender-IP simulator from the same response.
What SPF Record does
Returns the raw SPF string, parsed mechanisms / qualifiers / directives, optional recursive include expansion, RFC 7208 compliance checks (with `tests=true`), and AI-generated findings. The response is the ground truth; the UI computes derived analytics (health score, policy strength, 10-lookup / 2-void meters, sender inventory, provider fingerprint, sender-IP simulator) on the client using `src/features/domain/SpfLookup/spf-analytics.js`.
- Method GET
- Path /api/v1/domain/spf
- Stability STABLE
- Version v2026-05-15
- Summary Fetch and parse the SPF record for a domain.
Request parameters
Pass these as query-string parameters (or, where indicated, in the request headers). Required params marked required.
- domain (required) — string Apex domain.
- tests — boolean, default false Run additional best-practice checks (DNS lookup count, void lookups, syntax).
- expand — boolean, default false Recursively expand `include:` references and resolve `ip4` / `ip6` mechanisms.
Response schema
All successful responses follow the standard `{ success: true, data: {...} }` envelope. Notable fields on the data object for this endpoint:
- data.spf — string Raw SPF TXT record (v=spf1 …).
- data.parsed.version — string Always `v=spf1` for valid records.
- data.parsed.mechanisms — object Buckets for `ip4`, `ip6`, `include`, `a`, `mx`, etc. plus qualifier + directives arrays.
- data.parsed.qualifier — string Terminal `all` qualifier: `-all`, `~all`, `?all`, or `+all`.
- data.tests — array<object> RFC 7208 compliance checks. Empty unless `tests=true`. Each item: `{ test, result, passed }`. Includes: Multiple SPF Records, Too Many DNS Lookups (with `N/10` in the result), SPF Policy Strength, No 'PTR' Mechanism, SPF Record Length, Number of Void Lookups, SPF record validity, No Deprecated Mechanisms.
- data.expanded — object Populated only with `expand=true`. Recursive tree: `{ domain, spfRecord, lookupCount, ip4List, ip6List, mechanisms, includes: [{ domain, expanded: {...} }] }`. Feeds the client-side simulator + inventory.
- data.AiAnalysis — object | null Present only when the AI review is cached (cache hit). Shape: `{ issues: [{ severity, title, description, recommendation }], quick_recommendations: [string] }`. Severity is one of `info | warning | critical`.
- data.AiAnalysisMeta — object | null Present on cache miss instead of `AiAnalysis`. Shape: `{ id, avgMs }`. Poll `/ai/analysis/:id` (via `useAiAnalysisPoll`) until the review completes, then merge the resulting object into `AiAnalysis`.
Example response
Truncated but representative example for a real lookup:
{
"success": true,
"data": {
"spf": "v=spf1 include:_spf.google.com ~all",
"parsed": {
"version": "v=spf1",
"mechanisms": { "ip4": [], "include": ["_spf.google.com"] },
"modifiers": [],
"qualifier": "~all",
"directives": ["include:_spf.google.com", "~all"]
},
"tests": [
{ "test": "Multiple SPF Records", "result": "Only one SPF record found.", "passed": true },
{ "test": "Too Many DNS Lookups", "result": "DNS lookups within limit (4/10).", "passed": true },
{ "test": "SPF Policy Strength", "result": "SoftFail (~all) policy used.", "passed": true },
{ "test": "No 'PTR' Mechanism", "result": "No type 'PTR' found.", "passed": true },
{ "test": "SPF Record Length", "result": "String length is OK.", "passed": true },
{ "test": "Number of Void Lookups", "result": "Number of void lookups is OK. (0)", "passed": true },
{ "test": "SPF record validity", "result": "Valid", "passed": true },
{ "test": "No Deprecated Mechanisms", "result": "No deprecated records found.", "passed": true }
],
"expanded": {
"domain": "example.com",
"spfRecord": "v=spf1 include:_spf.google.com ~all",
"lookupCount": 4,
"ip4List": [],
"ip6List": [],
"mechanisms": [
{ "type": "include", "qualifier": "+", "value": "_spf.google.com" },
{ "type": "all", "qualifier": "~", "value": null }
],
"includes": [
{ "domain": "_spf.google.com", "expanded": { "...": "recursive" } }
]
},
"AiAnalysisMeta": { "id": "a1b2c3", "avgMs": 4200 }
}
}Error codes
Errors return HTTP status codes paired with stable machine-readable codes. Match on the code field rather than the human-readable message.
- 404 NO_SPF_RECORD Domain has no SPF TXT record.
Implementation tips
Patterns we recommend when integrating this endpoint:
- Pass `tests=true` for production audits — catches >10 lookups, void lookups, deprecated mechanisms, policy strength.
- Use `expand=true` when you need the resolved chain (required by sender inventory, provider fingerprint, IP simulator). Adds latency proportional to chain depth; safe to cache for hours.
- First-request `AiAnalysisMeta` returned instead of `AiAnalysis` — poll the analysis endpoint or reload after ~5s for the cached result.
- Compute derived analytics (health score, policy strength, lookup / void meters, sender inventory, provider fingerprint, sender-IP simulator) client-side via `spf-analytics.js`. Keeps the API stateless and lets you re-derive without a network round-trip.
Frequently asked questions
- What does the qualifier `~all` mean? Soft-fail: mail from unauthorized senders is marked but not rejected. `-all` is the strict equivalent.
- Why did I get `AiAnalysisMeta` instead of `AiAnalysis`? First request for a given domain — the analyzer runs in the background. Poll the meta id or refresh after ~5s and you'll get the cached `AiAnalysis` object. Meta is auto-included; you don't need to opt in.
- How do I evaluate a specific sender IP against this SPF? Call the endpoint with `expand=true`, then pass `data.expanded` and the IP to `simulateSender()` in `spf-analytics.js`. It walks the resolved chain client-side (no extra DNS lookups) and returns `{ result, matchedBy, matchedIn, chain }`. Full IPv4 + IPv6 CIDR matching.
- Where does the health score come from? Client-side. `computeHealth({ tests, lookupCount, voidCount, policy })` weights the compliance-test pass ratio, penalises lookup pressure (hard cliff at 10), void-lookup pressure (hard cap at 2) and weak policy (`+all` heavy penalty, `?all`/missing lesser). Returns `{ score, band, tone }`.