Skip to content
Reference · REST API v1

Reports API

Access DMARC aggregate report data programmatically: individual reports with their per-source records, rolled-up statistics, file upload, and CSV export.

01 — Read this first

Retention shapes every response

An empty array often means “outside retention”
Reports older than your plan's retention window are filtered out server-side, and any days parameter you send is silently capped to it. Monitor keeps 30 days and is additionally limited to the 10 most recent reports with no pagination; the paid plans keep 1 year on Protect, Growth, Professional and the Partner plans.
02 — The surface

Endpoints

GET/api/v1/reportsList DMARC aggregate reports
GET/api/v1/reports/{report_id}Get report details
GET/api/v1/reports/stats/summaryGet aggregated report statistics
POST/api/v1/reports/uploadUpload a DMARC report file
GET/api/v1/reports/export/csvExport report records as CSV
03 — GET /api/v1/reports

List DMARC reports

Returns reports for your organization, most recently ingested first, each with its full records array.

Request
curl -X GET "https://api.ddmarc.com/api/v1/reports?domain_id=42&limit=50&offset=0" \
  -H "X-API-Key: $DDMARC_API_KEY"
ParameterTypeDefaultDescription
domain_idintegerallRestrict to one domain.
limitinteger50Rows to return. Hard ceiling of 200.
offsetinteger0Rows to skip.
There are no date-range parameters on this endpoint. To bound a window, use /reports/stats/summary?days= or the CSV export, both of which accept days.
200 OK
[
  {
    "id": 9012,
    "domain_id": 42,
    "report_id": "16058034545453596399",
    "org_name": "google.com",
    "date_range_begin": "2026-07-17T00:00:00Z",
    "date_range_end": "2026-07-17T23:59:59Z",
    "policy_domain": "example.com",
    "policy_adkim": "r",
    "policy_aspf": "r",
    "policy_p": "reject",
    "policy_sp": null,
    "policy_pct": 100,
    "created_at": "2026-07-18T09:04:11Z",
    "records": [
      {
        "id": 551201,
        "source_ip": "209.85.220.41",
        "count": 843,
        "disposition": "none",
        "dkim_result": "pass",
        "spf_result": "pass",
        "header_from": "example.com",
        "envelope_from": "example.com",
        "envelope_to": null
      }
    ]
  }
]
04 — GET /api/v1/reports/{report_id}

Get a single report

Returns one report with all of its records, in the same shape as an element of the list response.

Which id?
{report_id} here is the integer id field — not the reporter's own report_id string.
05 — GET /api/v1/reports/stats/summary

Summary statistics

Rolls up volume and pass rates over a time window, without paging through every report.

ParameterTypeDefaultDescription
domain_idintegerallRestrict to one domain.
daysinteger30Look-back window, 1 to 365. Capped to your plan's retention.
200 OK
{
  "total_reports": 184,
  "total_emails": 412903,
  "pass_rate": 98.4,
  "dkim_pass_rate": 99.1,
  "spf_pass_rate": 97.2,
  "top_sources": [
    { "source_ip": "209.85.220.41", "total": 180422 }
  ]
}

Rates are percentages rounded to one decimal place. A message counts as passing only when both DKIM and SPF pass.

06 — POST /api/v1/reports/upload

Upload a report file

Ingests a DMARC aggregate report you already hold as a file. Accepts raw XML, ZIP or GZIP as a multipart upload, up to 10 MB. Requires a key with write scope, and is limited to 30 uploads per minute.

Request
curl -X POST "https://api.ddmarc.com/api/v1/reports/upload" \
  -H "X-API-Key: $DDMARC_API_KEY" \
  -F "file=@google.com!example.com!1752710400!1752796799.xml.gz"
200 OK
{
  "message": "1 report(s) uploaded successfully",
  "uploaded": [
    {
      "report_id": "16058034545453596399",
      "domain": "example.com",
      "org_name": "google.com",
      "records_count": 12,
      "total_emails": 1843
    }
  ],
  "duplicates_skipped": 0,
  "no_domain_skipped": 0
}
Nothing landing is an error, not an empty result
409 when every report in the file was already ingested, and 400 when the report's domain is not in your organization — add the domain first.
07 — GET /api/v1/reports/export/csv

CSV export

Export is a dedicated endpoint, not a format=csv parameter on other routes. It streams one row per DMARC record with the columns Date, Domain, Reporter, Source IP, Country, Organization, Count, Disposition, DKIM Result, SPF Result, Header From and Envelope From.

Request
curl -X GET "https://api.ddmarc.com/api/v1/reports/export/csv?domain_id=42&days=30" \
  -H "X-API-Key: $DDMARC_API_KEY" \
  -o dmarc-records.csv
Limits
Requires the Protect plan or higher. Ten exports per hour, capped at 500,000 rows per call — narrow days or domain_id if you are near that. Senders and the activity log have their own exports at /api/v1/senders/export/csv and /api/v1/activity/export/csv.
08 — Schema

The report object

FieldTypeDescription
idintegerReport identifier, used as {report_id} in paths
domain_idintegerDomain this report belongs to
report_idstringThe reporter's own report id, from the XML
org_namestringReporting organization, e.g. google.com
date_range_beginstringISO 8601 start of the reporting window
date_range_endstringISO 8601 end of the reporting window
policy_domainstringDomain the published policy was found on
policy_pstring | nullPublished policy: none, quarantine or reject
policy_spstring | nullSubdomain policy, if published
policy_adkimstring | nullDKIM alignment mode: r (relaxed) or s (strict)
policy_aspfstring | nullSPF alignment mode: r (relaxed) or s (strict)
policy_pctinteger | nullPercentage of mail the policy applied to
created_atstringISO 8601 ingestion timestamp
recordsarrayPer-source results; see the record object below
No pre-computed totals
A report carries no total or pass rate of its own. Sum records[].count yourself, or use the summary endpoint.
09 — Schema

The record object

FieldTypeDescription
idintegerRecord identifier
source_ipstringSending IP address
countintegerMessages this row represents
dispositionstring | nullWhat the receiver did: none, quarantine or reject
dkim_resultstring | nullDKIM evaluation: pass, fail, none, and so on
spf_resultstring | nullSPF evaluation: pass, fail, none, and so on
header_fromstring | nullDomain in the From header
envelope_fromstring | nullMAIL FROM domain, when the reporter supplies it
envelope_tostring | nullRCPT TO domain, when the reporter supplies it
10 — Going deeper

Analyzing failures

Raw reports tell you what happened; the failure-analysis endpoints tell you why. They live under their own prefix and each accept days (1–365, default 30) and an optional domain_id.

GET/api/v1/failure-analysis/summaryGet failure totals and split by mechanism
GET/api/v1/failure-analysis/root-causesGet classified root causes with fix suggestions
GET/api/v1/failure-analysis/root-causes/by-sourceGet root causes grouped by sending source
GET/api/v1/failure-analysis/sourcesList failing sources
GET/api/v1/failure-analysis/spoof-clustersGet suspected spoofing campaign clusters
GET/api/v1/failure-analysis/by-header-fromGet failures grouped by spoofed From identity
GET/api/v1/failure-analysis/trendGet failure trend over time
GET/api/v1/failure-analysis/resolution-statsGet time-to-resolution statistics
GET /api/v1/failure-analysis/summary · 200 OK
{
  "total_failures": 6604,
  "total_emails": 412903,
  "failure_rate": 1.6,
  "period_days": 30,
  "by_reason": [
    {
      "reason": "spf_only",
      "display_name": "SPF Only Failed",
      "count": 4102,
      "percentage": 62.1,
      "description": "SPF authentication failed but DKIM passed. Check SPF records and authorized senders."
    }
  ]
}
11 — Elsewhere

Looking for charts and breakdowns?

Trend lines, geographic distribution, volume heatmaps, top sources, protocol breakdown and the compliance scorecard are served from /api/v1/dashboard/*, not from the reports endpoints. Per-source detail lives under /api/v1/senders. Both are listed on the API overview.