API
dotMARC has a JSON API for the things MSPs most often script: listing domains and their DNS health, pulling DMARC report summaries for client reporting, adding or importing domains, organising them into groups and tags, and acknowledging alerts. Everything else stays in the web app for now.
The API reference lists every endpoint with its request and response shapes and examples. The
same OpenAPI document is served by your dotMARC at /api/v1/openapi.json,
which most API tools (Postman, Insomnia, Bruno and the like) can import directly.
Create a key
On the Access page's API keys tab, give the key a name, choose a role and how long it lasts (30, 90, 180 or 365 days), and select Create key. Copy the key straight away: dotMARC keeps only a hash of it, so it can't be shown again.
- A key can do what its role allows. For a Viewer you can also limit the key to certain groups, so it only sees those clients' domains.
- Roles that can manage access aren't offered. Keys can't create keys or change who has access.
- A key can't be edited. To change what it can do, create a new key and revoke the old one.
- Two weeks before a key expires, dotMARC raises an API key expiring alert through your alert channels, once rather than every cooldown, while alerting is turned on. It doesn't open a ticket unless you turn that on.
- Changes made with a key appear in the audit log as the key, with the name of the person who created it.
Authenticate
Send the key as a bearer token:
curl https://dotmarc.example.com/api/v1/domains \
-H "Authorization: Bearer dmk_..."
A missing, expired or revoked key gets 401. A key whose role lacks the permission an endpoint needs gets 403. The
API ignores browser sign-ins: it only accepts keys.
Finding a domain
Anywhere a path takes a domain, such as /api/v1/domains/{domain}, you can give its id or its name, so a script that
knows the name doesn't need to look up the id first:
curl https://dotmarc.example.com/api/v1/domains/contoso.example -H "Authorization: Bearer dmk_..."
Names match however they're written: any case, with or without a trailing dot. A domain name can never be all digits, so a number is always read as an id.
Filtering
The list endpoints take filters as query parameters, so a script asks for just what it needs. Every filter is in the API reference with the values it accepts.
- Several values: give a filter more than one value with commas (
type=SpfRecordBroken,DkimRecordBroken) or by repeating it (type=SpfRecordBroken&type=DkimRecordBroken). A record matches if it matches any of them. - Several filters: a record must match every filter you give.
- Letter case doesn't matter:
spf=toomanylookupsworks. - Domains can be given by id or name. A domain the key can't see is a 404.
- Dates are a day (
2026-10-01, from midnight UTC) or a time with its zone (2026-10-01T09:30:00Z). Filters endingAfterinclude the time you give; filters endingBeforedon't, so back-to-back ranges never overlap. In a URL a+offset should be sent as%2B(2026-10-01T10:30:00%2B01:00); dotMARC also reads an unencoded+, which arrives as a space, the same way. - A value dotMARC doesn't recognise is a 400 that names the filter and lists the values it accepts.
Some examples:
| To get | Ask for |
|---|---|
| Domains in group 3 whose SPF has too many lookups or is misconfigured | GET /api/v1/domains?group=3&spf=TooManyLookups,Misconfigured |
| Domains that need attention, as the client portal shows them | GET /api/v1/domains?status=NeedsAttention |
| Open SPF and DKIM alerts raised this week | GET /api/v1/alerts?type=SpfRecordBroken,DkimRecordBroken&raisedAfter=2026-10-05 |
| Everything that happened to one domain | GET /api/v1/audit?domain=contoso.example |
| Sign-ins in September | GET /api/v1/audit?kind=signIn&occurredAfter=2026-09-01&occurredBefore=2026-10-01 |
The audit log
GET /api/v1/audit needs the AuditView permission. It returns changes and sign-ins by default; add kind=pageView or
kind=all for pages people opened. A key limited to certain groups sees only entries about its own domains, its groups
and their alerts, never sign-ins, settings or access changes.
Reports and downloads
-
Sending sources:
GET /api/v1/domains/{domain}/sourceslists every IP that sent as the domain, busiest first, with its pass rate and, once dotMARC has looked it up, who owns it. The report summary only shows the top few. -
Individual reports:
GET /api/v1/domains/{domain}/reportsand.../tls-reportslist the DMARC and TLS reports received. Add/{id}/rawto one report's address for the original file, exactly as the receiver sent it. -
The dashboard's figures:
GET /api/v1/summarygives the headline numbers over every domain the key can see. -
Client reports:
GET /api/v1/groups/{group}/report?period=month&of=2026-09-15downloads a group's report as a PDF, andPOST /api/v1/groups/{group}/report/sendsends one now, as the Reports dialog does. For example:curl -H "Authorization: Bearer dmk_..." -o report.pdf \"https://dotmarc.contoso.example/api/v1/groups/3/report?period=quarter&of=2026-07-01" -
Parse failures:
GET /api/v1/parse-failureslists report emails dotMARC couldn't read, for a key with the DomainsEdit permission that isn't limited to groups. -
PSA tickets: each alert lists its tickets, and
hasOpenTicket=falsefinds alerts without an open ticket: ones no ticket has been raised for, and ones whose tickets are all closed.
Keys limited to groups
A key limited to groups sees only domains in at least one of its groups, and only those groups. A domain or alert outside
them is reported as not found (404). It can't add or import domains, since a new domain isn't in any group yet. When it
sets a domain's groups, groups outside its own are kept.
Limits
Each key can make 120 requests a minute. Over that, dotMARC answers 429 with a Retry-After header giving the seconds
to wait. Lists return 50 items a page by default and at most 200; use page and pageSize.
Health comes from dotMARC's own scheduled checks, so reading it is quick and never queries DNS; each check says when it last ran.
Errors
Errors use the standard problem details format (application/problem+json), with a title, a detail saying what to
do, and for invalid input an errors object naming each field.
| Status | Meaning |
|---|---|
| 400 | The request is invalid. errors says which field. |
| 401 | No key, or the key is unknown, expired or revoked. |
| 403 | The key's role doesn't allow this, or a group-limited key tried something it can't. |
| 404 | No such item, or the key can't see it. |
| 405 | The path exists but not with that method. The Allow header lists the ones it accepts. |
| 409 | The domain already exists, or the alert can't be acknowledged. |
| 429 | Too many requests this minute. Wait for Retry-After. |
Importing domains
POST /api/v1/domains/import takes up to 500 domains and behaves like the Import domains page.
existingDomains is skip (default), add or match, and unknownNames is skip (default) or create. In match
mode, send groups and tags on every domain or on none: an empty list clears them, and a domain that left them out
while others sent them would otherwise be cleared by mistake, so that's refused. Add ?dryRun=true to see what would
happen without changing anything.