What you will build
DMARC aggregate reports tell you who sends email with your domain and whether it authenticates. Sender Audit collects them, parses them and keeps the figures per domain and per day. The API gives you the same figures as the dashboard, for example to:
- feed a weekly report or a BI dashboard with the compliance rate of each domain;
- alert when the share of failing messages jumps, or when a regular reporter goes silent;
- audit a portfolio of domains: which ones still sit at
p=none, which ones send their reports nowhere.
This guide goes from the summary to the detail: posture of every domain, volumes over time, then the sources of one day and the raw reports.
Before you start
- An API key created from the dashboard, under Account, Developer, on a plan that includes API access.
- Domains covered by the account. Reports are only kept for domains verified on the account. You do not always need to verify first: when a report arrives for a new domain whose DMARC record lists your collection address, that record is the proof, and the domain is verified automatically (within your plan’s quota). To verify one explicitly, see API: monitor your domains.
- Reports coming in. Your domain’s DMARC record must send its reports to your collection address (step 1). Count 24 to 72 hours before the first ones arrive: receivers send them once a day.
export SA_KEY="sa_live_your_key"
export SA_API="https://senderaudit.com/api/v1"
import os, requests
API = "https://senderaudit.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SA_KEY']}"
Most endpoints accept the same filters:
| Parameter | Effect |
|---|---|
domain | One domain, by name: example.com |
group | The domains of one resource group, by its id |
days | Window in whole UTC days, 30 by default |
Without domain or group, figures cover every domain of the account. How far back you can go depends on your plan: responses carry it as history_days.
Step 1: get the reports flowing
GET /dmarc/address returns your account’s collection address, rua_address. It goes in the rua= tag of the DMARC record published at _dmarc.example.com:
v=DMARC1; p=none; rua=mailto:your-address@rua.senderaudit.com
To check that a domain’s record really points to it, call GET /dmarc/record-check?domain=example.com. It resolves the record live and answers our_rua_present: true once the address is in place. rua_count_warning flags a record that already lists many addresses, which some receivers refuse to honour.
curl -s "$SA_API/dmarc/record-check?domain=example.com" -H "Authorization: Bearer $SA_KEY"
GET /domains/{id}/setup goes one step further: it returns the exact DMARC record to publish, completed with your address rather than replacing what is there.
Step 2: the posture of every domain
GET /dmarc/domains-summary answers “where does each domain stand?” in one call, grouped by organizational domain.
curl -s "$SA_API/dmarc/domains-summary" -H "Authorization: Bearer $SA_KEY" | jq .totals
summary = session.get(f"{API}/dmarc/domains-summary").json()
print(summary["totals"])
for group in summary["groups"]:
for d in group["domains"]:
print(d["domain"], d.get("policy"), d["rua_status"], d["reports_30d"])
totals counts the domains per policy (reject, quarantine, none, no_policy), and those whose reports go nowhere useful (no_rua). For each domain:
| Field | What it is |
|---|---|
policy | Published policy: reject, quarantine, none, or null when there is no record |
inherited | true when the domain has no record of its own and falls under its organizational domain’s |
rua_status | Where its reports go: received (to you), via_parent (through the parent’s record), elsewhere, none, or other_path (reports arrive although DNS does not point to you) |
reports_30d | Reports actually received over 30 days, whatever DNS says |
The values come from the last monitoring cycle, not from a live DNS lookup. Use record-check to force a live check on one domain.
Step 3: the key figures
GET /dmarc/overview returns what the synthesis screen shows above the fold, for the window you ask.
curl -s "$SA_API/dmarc/overview?domain=example.com&days=30" -H "Authorization: Bearer $SA_KEY" | jq .kpis
overview = session.get(f"{API}/dmarc/overview", params={"domain": "example.com", "days": 30}).json()
kpis = overview["kpis"]
{
"messages": 184230,
"compliance": 0.9874,
"spf_aligned": 0.9411,
"dkim_aligned": 0.9802,
"alignment_ready": true
}
messages: volume described by the reports over the window.compliance: share of that volume that is legitimate, between 0 and 1. Identified forwarders count as legitimate: they break alignment by design, and you do not control them.spf_aligned,dkim_aligned: share of the volume aligned on each mechanism.nullwhilealignment_readyisfalse, which means “not computed yet”, never zero.complianceisnullon an empty window: no volume is not a rate of zero.
The same response breaks the volume down by sender role in buckets (legitimate, forwarder, unauthorized, unclassified…), and series gives one value per day for the main senders, ready to chart.
Step 4: volumes over time
Two endpoints, depending on whether you want the account as a whole or one line per domain.
GET /dmarc/stats: one row per day, all selected domains together.
{ "day": 20741, "date": "2026-10-14", "pass": 6120, "forwarded": 84, "fail": 37 }
GET /dmarc/daily: one row per domain and per day, with what receivers did with the messages (disp_none, disp_quarantine, disp_reject).
rows = session.get(f"{API}/dmarc/daily", params={"days": 7}).json()["rows"]
for r in rows:
fail_rate = r["fail"] / r["total"] if r["total"] else 0
if fail_rate > 0.05:
print(f'{r["date"]} {r["domain"]}: {fail_rate:.1%} failing, {r["disp_reject"]} rejected')
The three counters mean:
| Counter | Meaning |
|---|---|
pass | SPF or DKIM passed, aligned with your domain |
forwarded | Failed, but relayed by an identified forwarder or mailing list. Not your fault, and usually not fixable on your side |
fail | Failed with no known excuse: a misconfigured platform of yours, or someone spoofing you |
Reports arrive with a delay, mostly the next day, sometimes up to a week later. Yesterday’s figures keep growing for a few days: compare complete days.
Going further: the detail
The sources of one day
When a day looks wrong, GET /dmarc/daily/detail lists who sent for one domain on one date, aggregated by source IP.
curl -s "$SA_API/dmarc/daily/detail?domain=example.com&date=2026-10-14&status=fail&limit=50" \
-H "Authorization: Bearer $SA_KEY"
Each source carries its IP, the name of the service behind it when known (source_display), its country, the SPF and DKIM results, the From and envelope domains, the receivers’ disposition and the message count. status filters on pass, forwarded, fail (comma separated), q searches the IP, names and domains. Without limit, the whole day comes back, which can be thousands of sources on a large mailbox domain: page with limit and offset.
Who sends on your behalf
GET /dmarc/senders returns the sender catalogue over the window, grouped by role: legitimate, forwarder, unauthorized, unclassified, no_rdns, ignored. Each sender has its volumes (total_msgs, pass_msgs, fail_msgs, pass_rate), the dispositions applied, and its first and last appearance. unclassified with a large volume is where to look first.
Classifying them is a workflow of its own, with the evidence to decide on each source: see API: classify who sends on your behalf.
Who reports
GET /dmarc/reporters lists the organisations that send you reports (Google, Microsoft, Yahoo…), with a status: active, occasional, or silent when a regular reporter has sent nothing for more than three days. A silent major receiver usually means a DNS change broke your rua=.
The raw reports
GET /dmarc/reportslists the reports received, filtered bydomain,groupororg(the reporter’s name), paged withlimit(200 max) andoffset. Dates are Unix timestamps.GET /dmarc/reports/{report_id}returns one report with all its records, as the receiver sent it.
Domains you do not monitor yet
GET /dmarc/unmatched-domains lists domains for which reports reached your address without being covered by the account. Those reports were not kept: verify the domain to collect the next ones.
Limits and errors
| Situation | Answer |
|---|---|
| Rate limit reached (20 to 60 calls per minute depending on the endpoint) | 429, with a Retry-After header |
record-check on a domain not verified on the account | 403 |
| Figures for a domain the account does not cover | Empty results, not an error |
| Missing or invalid key | 401 |
| Plan without API access | 403 |
GET /dmarc/quota returns the month’s consumption of aggregated DMARC messages against your plan’s limit, when the plan has one.
Related
- API reference, DMARC: every field, and a console to try the calls
- API: classify who sends on your behalf
- API: monitor your domains
- API: test your emails before sending