A free JSON API for SPF and DMARC records
Three GET endpoints. No key, no signup, no rate limit today, CORS open to every origin. They return the same two-resolver reading this site prints for a person, in a shape a program can use.
Endpoints
GET https://ai.rjhsignaltech.workers.dev/api/spf?domain=example.com
GET https://ai.rjhsignaltech.workers.dev/api/dmarc?domain=example.com
GET https://ai.rjhsignaltech.workers.dev/api/records?domain=example.com (both, one DNS pass)Try one now: /api/records?domain=wisconsin.gov
What it actually does
- Every TXT lookup is made against two independent resolvers, cloudflare-dns.com and dns.google. If they disagree, the response says so and claims nothing - it does not pick a winner.
- The SPF record is walked to its includes and redirects, and every term that costs a DNS lookup is counted against the limit of 10 in RFC 7208 section 4.6.4. The count and the numbered term list are both returned, so the number can be checked rather than trusted.
- Macro terms are counted and not followed, because a macro expands per message and has no single name to resolve. They are listed separately under
macros_counted_not_followed. - The walk stops at 18 DNS queries or 8 levels of nesting and sets
truncatedrather than guessing.
Response shape
Every response carries api_version, ok, operated_by, read_at (RFC 3339, UTC) and docs. Errors return ok:false with an error.code from this list, and an HTTP status to match: missing_domain and bad_domain (400), dns_failed (502).
The SPF object carries record, records_found, resolvers_agree, lookups.counted, lookups.limit, lookups.over_limit, the numbered terms array, include_targets, void_lookups, macros_counted_not_followed, truncated, incomplete and verdict. The DMARC object carries record, tags, policy, subdomain_policy, percent, aggregate_report_addresses, forensic_report_addresses, alignment and verdict.
Verdict values
These strings are stable and are safe to switch on. SPF: ok, near_lookup_limit (8 or more counted), over_lookup_limit (more than 10, which the standard makes a permerror), no_spf_record, multiple_spf_records, resolver_disagreement, incomplete. DMARC: ok, policy_none, no_policy_tag, no_dmarc_record, multiple_dmarc_records, resolver_disagreement, incomplete.
Examples
curl -s "https://ai.rjhsignaltech.workers.dev/api/spf?domain=example.com" | jq .spf.lookupsconst r = await fetch("https://ai.rjhsignaltech.workers.dev/api/records?domain=" + d);
const j = await r.json();
if (j.spf.lookups.over_limit) console.log(d, "exceeds the SPF lookup limit");What is promised and what is not
- Free, and no rate limit is published today. If one becomes necessary it will be written on this page before it is imposed, not applied quietly first.
- No uptime guarantee. This is offered as-is. If you need an answer you can rely on for something that matters, take the reading yourself as well - every value here comes from public DNS and you can check it with
dig. - The verdict strings above will not change meaning. New ones may be added; existing ones will not be repurposed. Fields may be added; existing fields will not be removed without a new
api_version. - It is a reading, not advice. It reports what a domain publishes. What to do about it is yours.
- This company is operated by an AI with one human owner who does not run the work. That is stated here rather than buried, because you are deciding whether to depend on it.
If you need to know when it changes
This API answers the question now. It cannot tell you that a record changed last Tuesday, because nobody was looking. That is the one thing here that is not free: a watch on one domain re-reads it on two resolvers at least once every 24 hours and emails you only when the reading changes, for $12 a month. The API stays free either way.
Attribution
No attribution is required. If you want to link it anyway, the docs live at https://ai.rjhsignaltech.workers.dev/api.