DMARC Reports API
DMARC Reports API, webhooks and AI connector.
See who sends email as your domains, how much passes and what to fix, from your own code and dashboards. Get told when a report arrives or a new sender appears, or ask Claude about it in plain English.
1Get started
The API comes with DMARC Reports 1.2.0 and later. It lives on your own install:
https://dmarc.yourdomain.co.uk/api/v1
- Open SettingsSign in to DMARC Reports and open Settings. Your API address is under API, webhooks and AI connector, ready to copy.
- Make a keyGive it a name, tick It can change things if your script needs to add domains or mark senders, and copy the key. It's shown once, and starts with
dmarc_. - Send it with each requestIn an Authorization header, like this:
curl https://dmarc.yourdomain.co.uk/api/v1/domains \ -H "Authorization: Bearer dmarc_••••••••••••••••••••••••••••••••"
2Keys and permissions
Make a separate key for each script or app. You can have up to 10.
| Key | What it allows |
|---|---|
| Read only | Your domains, senders, reports and TLS results. |
| Read and change | All of that, plus adding domains and marking senders Mine or Not mine. |
3Every request
Requests that cover a period take days: 7, 30 (the default), 90 or 365.
| Request | Needs | What it does |
|---|---|---|
GET /domains | Read | Your domains, with the last 30 days' reports, messages and pass rate, and the address reports go to. |
POST /domains | Change | Add a domain: {"domain": "yourdomain.co.uk"}. |
GET /domains/{domain} | Read | Who sent email as the domain, how much passed, its policy and what to fix. |
GET /domains/{domain}/senders | Read | Every sender in full, with its verdict, the IP addresses it used and the fix. |
POST /domains/{domain}/senders | Change | Mark a sender: {"key": "…", "label": "mine"}. Label is mine, not or null to clear it. |
GET /domains/{domain}/reports | Read | The latest reports that arrived. Takes limit (up to 100). |
GET /domains/{domain}/tls | Read | TLS reports: how much email to you was sent encrypted, and any failures. |
4Examples
How is a domain doing?
curl "https://dmarc.yourdomain.co.uk/api/v1/domains/yourdomain.co.uk?days=30" \ -H "Authorization: Bearer $DMARC_KEY"
The answer (shortened)
{
"domain": "yourdomain.co.uk",
"days": 30,
"policy": "none",
"messages": 4210,
"passed": 4102,
"passRate": 97,
"senders": [
{ "key": "google", "name": "Google Workspace", "label": "mine", "status": "good", "messages": 3890, "passed": 3890, "passRate": 100 },
{ "key": "mailchimp", "name": "Mailchimp", "label": null, "status": "fix", "messages": 320, "passed": 212, "passRate": 66 }
]
}Mark a sender as yours
curl https://dmarc.yourdomain.co.uk/api/v1/domains/yourdomain.co.uk/senders \
-H "Authorization: Bearer $DMARC_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "mailchimp", "label": "mine" }'In JavaScript
const res = await fetch(`${DMARC_API_URL}/domains`, {
headers: { Authorization: `Bearer ${process.env.DMARC_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const { domains } = await res.json();5Webhooks
Add up to 5 webhooks in Settings › API, webhooks and AI connector, each with its own https:// address.
| Event | When it's sent |
|---|---|
report.received | A DMARC or TLS report arrived by email and was saved. |
sender.new | Someone new started sending email as one of your domains. |
sender.failing | A sender that's yours started failing DMARC. |
reports.stopped | No reports for 3 days. |
tls.failing | Mail servers say email to you couldn't be sent encrypted. |
test | The Test button. |
What DMARC Reports sends
{
"id": "evt_4b7c2a9e1f03d6a8c5e2b1f0",
"type": "report.received",
"created": "2026-11-04T06:12:40Z",
"app": "DMARC Reports",
"data": {
"kind": "dmarc",
"domain": "yourdomain.co.uk",
"org": "google.com",
"reportId": "1234567890123456789",
"begin": "2026-11-03T00:00:00Z",
"end": "2026-11-03T23:59:59Z",
"messages": 140,
"passed": 138
}
}Alerts carry domain, subject, text and a link to the domain in the app. Each request also carries DMARC-Reports-Event, DMARC-Reports-Delivery and DMARC-Reports-Signature. Answer with any 2xx status within 10 seconds. A webhook that fails 10 times in a row is switched off, and the Test button switches it back on.
Checking a webhook is genuine
Each webhook has its own signing secret (starting whsec_), shown when you add it. The app signs <t>.<body> with HMAC-SHA256 using that secret, and sends the result in the DMARC-Reports-Signature header as t=1760520601,v1=5f2b…. To check a request, work out the same signature from the raw body, compare it with v1 and refuse anything more than five minutes old.
Node.js
import crypto from "node:crypto";
function isFromDmarcReports(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.v1 || age > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Python
import hashlib, hmac, time
def is_from_dmarc_reports(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if "v1" not in parts or abs(time.time() - int(parts["t"])) > 300:
return False
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Use the body exactly as it arrived. Turning the JSON into an object and back into text can change it, and then the signature won't match.
6AI connector
Add DMARC Reports to Claude, ChatGPT or any MCP client. In Claude, open Settings › Connectors › Add custom connector and paste:
https://dmarc.yourdomain.co.uk/mcp
Claude asks you to sign in once. Then ask things like “who is sending email as my domain that isn't me?” or “am I ready for p=reject?”. It can only read. Disconnect AI apps in Settings signs it out. Apps that let you add a header can send a dmarc_ key instead.
7Errors and limits
Anything that goes wrong comes back with a status, a sentence you can show to a person and a link to this page:
{"error": "That domain isn't in your list. Add it first.", "docs": "https://paulobrien.dev/dmarc/api/"}| Status | What it means |
|---|---|
400 | Something in the request needs fixing, such as a key put in the web address. |
401 | No key, or a key that has been deleted. |
403 | The key can only read. |
404 | A domain that isn't in your list, or a request the API doesn't have. |
405 | Use GET to read and POST to change. |
429 | Each key can make 60 requests a minute. Retry-After says how long to wait. |
Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining. There are no browser (CORS) headers on purpose: keep keys on your server, or in tools like Zapier, Make or n8n.
Questions or ideas? Ask in GitHub Discussions, or open an issue for a bug.
Stay in the loop
Subscribe for updates.
Leave your email and I'll tell you when there's a new version of one of my apps, or a new project to try. Nothing else.