Developers
MailWatch API
Read your domains, DNS analysis, DMARC reports and statistics, sending sources and alerts, and add domains from your own tools. The API is included in the Business and Agency plans.
Authentication
Owners and admins create tokens under Organization → API. A token belongs to one organization and acts for the member who created it: it stops working when they leave the organization or are no longer an owner or admin. Send it as a bearer token on every request:
curl -H "Authorization: Bearer mw_…" https://mailwatch.eu/api/v1/domains
Read only tokens get domains.view, reports.view and alerts.view.
Read and write tokens can also add domains (domains.manage). A token never has more access than
its creator's role. Tokens can't manage members, billing or other tokens.
Conventions
- Base URL:
https://mailwatch.eu/api/v1. Request and response bodies are JSON. - Responses wrap results in
data. Paginated lists addmeta(page,per_pageup to 100,total,pages). - Times are ISO 8601 in UTC (
2026-09-29T12:00:00Z); days areYYYY-MM-DD. Rates are percentages (0–100) ornullwithout messages. - Periods (
?from=&to=) are inclusive UTC days, at most 366 days, 30 days by default. They start no earlier than your plan's history; the response'speriodshows the period actually used.
Errors
Errors use RFC 9457 problem details (application/problem+json):
{
"type": "about:blank",
"title": "Unprocessable Content",
"status": 422,
"detail": "The request body is invalid.",
"errors": [{"field": "name", "message": "This value should not be blank."}]
}
- 400
- Invalid query parameter or malformed JSON.
- 401
- Missing, unknown, expired or revoked token.
- 403
- The plan doesn't include the API or feature, the token's scope or creator's role doesn't allow it, or a plan limit was reached.
- 404
- Not found in the token's organization.
- 409
- The domain is already monitored.
- 415
- The body isn't sent as
Content-Type: application/json. - 422
- Validation failed; see
errors. - 429
- Rate limit exceeded; wait for
Retry-Afterseconds.
Rate limits
Each token can make 120 requests per minute. Every response reports the budget in
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time).
Domains
/domains
needs domains.view
All monitored domains of the organization. protocols holds the latest analysis status per protocol: pass, warning, fail or missing.
{
"data": [{
"id": "01928c5e-…",
"name": "example.com",
"display_name": "example.com",
"status": "active",
"reporting_address": "k3x9…@reports.mailwatch.eu",
"protocols": {"dmarc": "pass", "spf": "warning", "dkim": "pass", "mx": "pass"},
"verified_at": "2026-09-01T08:00:00Z",
"last_dns_check_at": "2026-09-29T06:00:00Z",
"created_at": "2026-09-01T07:58:12Z"
}]
}
/domains
needs domains.manage
Adds a domain and starts its first DNS check. Returns 201 with the domain and a Location header. Publish rua=mailto:<reporting_address> in its DMARC record to receive reports.
curl -X POST https://mailwatch.eu/api/v1/domains \
-H "Authorization: Bearer mw_…" \
-H "Content-Type: application/json" \
-d '{"name": "example.com"}'
/domains/{id}
needs domains.view
One domain, in the same form as the list.
DNS and analysis
/domains/{id}/dns
needs domains.view
The current DNS records MailWatch monitors (DMARC, SPF, DKIM selectors, MX, MTA-STS, TLS-RPT, BIMI) and the analysis of each protocol, with its findings.
{
"data": {
"last_check_at": "2026-09-29T06:00:00Z",
"records": [{
"name": "_dmarc.example.com", "type": "TXT", "status": "found",
"records": [{"value": "v=DMARC1; p=reject; rua=mailto:…", "ttl": 3600}],
"cname_chain": [], "dnssec_authenticated": false,
"changed_at": "2026-09-12T06:00:00Z", "last_seen_at": "2026-09-29T06:00:00Z"
}],
"analysis": {
"dmarc": {
"status": "pass", "summary": {"policy": "reject", …},
"findings": [{"severity": "warning", "code": "dmarc.policy_none", "message": "…"}],
"analyzed_at": "2026-09-29T06:00:01Z", "status_changed_at": "2026-09-12T06:00:01Z"
}
}
}
}
DMARC reports
/domains/{id}/dmarc
needs reports.view
Stored aggregate reports, newest period first, within the plan's history. Paginated with ?page= and ?per_page=.
{
"data": [{
"id": "…", "report_id": "1234567890",
"reporter": {"organization": "google.com", "email": "noreply-dmarc-support@google.com"},
"period": {"begin": "2026-09-28T00:00:00Z", "end": "2026-09-28T23:59:59Z"},
"policy_published": {"domain": "example.com", "adkim": "r", "aspf": "r", "p": "reject", "sp": null, "pct": 100},
"totals": {"messages": 1520, "dmarc_pass": 1498, "dmarc_fail": 22, "spf_aligned_pass": 1490,
"dkim_aligned_pass": 1497, "dmarc_pass_rate": 98.55, "spf_pass_rate": 98.03, "dkim_pass_rate": 98.49},
"records": 14,
"received_at": "2026-09-29T02:11:40Z"
}],
"meta": {"page": 1, "per_page": 25, "total": 88, "pages": 4}
}
Statistics
/domains/{id}/statistics?from=&to=
needs reports.view
DMARC totals for a period, and one entry per day (days without reports have zero messages).
{
"data": {
"period": {"from": "2026-08-31", "to": "2026-09-29"},
"summary": {"totals": {"messages": 45210, "dmarc_pass": 44870, …}, "sources": 31, "unknown_sources": 2},
"daily": [{"date": "2026-08-31", "totals": {"messages": 1480, …}}, …]
}
}
Sending sources
/domains/{id}/sources?from=&to=&limit=&unknown=
needs reports.view
Who sends as the domain: totals per provider (_unknown for unidentified sources, _pending while being identified), and the busiest source IPs (limit up to 500, default 100). unknown=true lists only sources that match no known provider.
{
"data": {
"period": {"from": "2026-08-31", "to": "2026-09-29"},
"providers": [{"key": "google", "name": "Google Workspace / Gmail", "sources": 12, "totals": {…}}],
"sources": [{
"ip": "209.85.220.41", "provider": "google", "reverse_dns": "mail-sor-f41.google.com",
"asn_name": "GOOGLE", "country": "US", "identified": true,
"first_seen": "2026-09-01", "last_seen": "2026-09-29", "totals": {…}
}]
}
}
Alerts
/domains/{id}/alerts?status=
needs alerts.view
Alerts of the domain, newest first. status is active (open and acknowledged, the default), open, acknowledged, resolved or all. Paginated.
{
"data": [{
"id": "…", "type": "dns_record_changed", "severity": "warning", "status": "open",
"title": "DMARC record changed on example.com", "body": "…", "context": {…},
"created_at": "2026-09-29T06:00:02Z", "acknowledged_at": null, "resolved_at": null
}],
"meta": {"page": 1, "per_page": 25, "total": 1, "pages": 1}
}
Webhooks
Instead of polling, let MailWatch push events to you. Owners and admins add HTTPS endpoints under Organization → API & webhooks → Webhooks (Business and Agency plans). Endpoints must use port 443 and a public host name.
alert.created- An alert was raised;
data.alerthas the same fields as the alerts endpoint. alert.resolved- An alert resolved itself because its condition cleared.
dns.changed- A DNS check found changed records;
data.changeslists each record with its old and new values. dmarc_report.processed- An aggregate report was stored;
data.reporthas its reporter, period and totals. ping- Sent by Send test event.
POST /your/endpoint
Content-Type: application/json
MailWatch-Event: alert.created
MailWatch-Delivery: 01928c5e-…
MailWatch-Signature: t=1790683200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
"id": "01928c5e-…",
"type": "alert.created",
"created_at": "2026-09-29T12:00:00Z",
"organization": {"id": "…", "name": "Acme", "slug": "acme"},
"data": {"domain": {"id": "…", "name": "example.com"}, "alert": {"type": "spf_lookup_limit", "severity": "critical", …}}
}
Verify every request. Compute HMAC-SHA256 over t + "." + raw body with the endpoint's signing secret,
compare it to v1 in constant time, and reject timestamps older than a few minutes:
$expected = hash_hmac('sha256', $t.'.'.$rawBody, $secret);
if (!hash_equals($expected, $v1) || abs(time() - $t) > 300) {
http_response_code(400);
exit;
}
Answer with a 2xx status within 10 seconds; redirects aren't followed. Other answers are retried after 1, 5 and 30 minutes, 2 and 6 hours.
After 15 events in a row fail, the endpoint is disabled until you enable it again. MailWatch-Delivery is unique per
delivery and the body's id per event, so you can drop duplicates.