DMARC Engine
Home/Blog/Automating DMARC, SPF and DKIM with the DMARC Engine API
Blog

Automating DMARC, SPF and DKIM with the DMARC Engine API

Most teams start with email authentication as a one-off project: someone fixes the SPF record, publishes a DKIM key, sets a DMARC policy, and moves on.

3 June 2026 · 16 min read

Automating DMARC, SPF and DKIM with the DMARC Engine API

Most teams start with email authentication as a one-off project: someone fixes the SPF record, publishes a DKIM key, sets a DMARC policy, and moves on. That works right up until the number of domains, sending services, or environments stops fitting in a person's head. At that point you want the same operations the dashboard performs (adding a domain, checking its records, watching its DMARC reports, reacting to a new sending source) to happen from your own code, your CI pipeline, or your provisioning scripts. The DMARC Engine API exists for exactly that. It is the same control plane the app at app.dmarcengine.com uses, exposed over HTTPS with API-key authentication so you can drive it programmatically.

This article is a practical guide to automating DMARC, SPF and DKIM with that API: how authentication with API keys works, the endpoints you will reach for most often, how to get your systems told about changes (through the notification channels and by polling), and worked example calls you can adapt. The aim is not to make you read every field in the reference, it is to give you a working mental model and enough concrete code to get the first integration running. Where a particular field or contract is not published, the detail is available on request from the team.

When automation actually earns its keep

Before any code, it is worth being honest about when the API is worth the effort, because for a single domain it usually is not. The dashboard is faster for one-off work, and the hosted model already removes most of the manual toil: when DMARC Engine manages your records by delegation, record edits, SPF flattening and change alerts happen for you without anyone touching DNS by hand.

The API earns its keep when you have scale or repetition that a human should not be in the loop for:

  • Agencies and MSPs onboarding domains for many clients, where adding a domain should be one step in an existing provisioning workflow rather than a separate manual login. If this is you, the partner and MSP arrangement (partner pricing, consolidated billing and reporting agreed with the team) is built around this pattern.
  • Platforms that sell to other businesses and want to authenticate their customers' sending sub-domains automatically as those customers sign up.
  • Infrastructure teams that treat DNS and email config as code, and want domain authentication state to live in the same version-controlled, reviewable pipeline as everything else.
  • Anyone running scheduled checks across a fleet of domains, exporting aggregate report data into their own data warehouse, or wiring DMARC alerts into an incident tool via the notification channels (Slack, PagerDuty or a generic webhook).

If none of those describe you, read the enforcement journey and use the dashboard. If one or more do, read on.

Authentication: API keys

Every API request authenticates with an API key. You create keys in the dashboard under Settings -> API keys (this is admin-only), and the key value is shown once at creation, so copy it then; it is stored only as a SHA-256 hash and cannot be retrieved again afterwards. A key is scoped to your account, so it can see and act on the domains that account owns and nothing else. Treat a key exactly as you would a password: anyone holding it can act as your account.

A few principles that matter in practice:

  • Generate a separate key per integration. One key for your provisioning service, another for your reporting export, another for the CI job. When you need to revoke one, you do not take down the others.
  • Give a key only to what needs it. A read-only reporting export and a provisioning job should not share a credential. Keeping keys separate limits the blast radius if one leaks.
  • Never put a key in client-side code or a public repository. The API is a server-to-server interface. A key embedded in a browser bundle, a mobile app, or a committed .env file is a key that is already compromised. Store keys in your platform's secret manager (for example, Cloudflare Worker secrets, GitHub Actions secrets, AWS Secrets Manager, or Vault) and inject them at runtime.
  • Rotate on a schedule and on suspicion. Rotation is cheap because keys are independent: create the new key, deploy it, then delete the old one once nothing is using it. There is no downtime if you do it in that order.

You pass the key in the x-api-key header on every request:

x-api-key: <your-api-key>

Requests without a valid key get 401 Unauthorized. Keep your error handling ready for that case: a 401 means fix your credential.

All requests go to the application's existing REST endpoints under https://app.dmarcengine.com/api/, over HTTPS only. Plain HTTP is refused; there is no negotiation down to an unencrypted connection, which is the correct behaviour for anything carrying a secret credential.

Conventions you can rely on

The API follows the conventions you would expect from a modern JSON HTTP interface, so you do not have to relearn anything:

  • JSON in, JSON out. Send Content-Type: application/json on requests with a body. Responses are JSON.
  • Standard HTTP verbs. GET to read, POST to create, PATCH to update, DELETE to remove. Reads never change state, so they are safe to retry freely.
  • Standard status codes. 2xx for success, 4xx for something you did wrong (bad input, missing auth, not found), 5xx for something on our side. Retry 5xx with exponential backoff; do not retry a 4xx without changing the request.
  • Be defensive on retries. Treat a failed write as needing a follow-up read to confirm the actual state before you blindly retry, rather than assuming the call did or did not take effect. For bulk work, spread requests out rather than firing one request per domain as fast as you can.

Every resource is identified by a stable ID. Store the ID, not the human-readable name, as your foreign key, because names can change and IDs do not. (The exact response shape and ID formats are available on request; the snippets below use illustrative placeholder values.)

The endpoints you will use most

You do not need the whole surface to be productive. The integration most people build touches a small, predictable set of resources. Here is the shape of each, with what it is for and the gotchas.

Domains

The domain is the central object. Almost everything else hangs off a domain ID.

  • GET /api/domains lists the domains on your account. Use it to reconcile your own inventory against ours.
  • POST /api/domains adds a domain. You send the domain name; you get back the domain object with its ID and its current authentication state. This is the call your onboarding workflow makes.
  • GET /api/domains/{id} returns the full state of one domain: its current SPF, DKIM and DMARC posture, whether DNS is delegated yet, the live policy (p=none, quarantine or reject), and any outstanding setup steps.
  • PATCH /api/domains/{id} updates managed settings for the domain, for example moving the DMARC policy forward as part of the journey to enforcement, or toggling features.
  • DELETE /api/domains/{id} removes a domain from management. Treat this as destructive and gate it behind a confirmation in your own tooling.

When you add a domain that you want hosted, the response tells you what DNS delegation is still required. The cleanest model is full DNS delegation, after which record management for hosted SPF, hosted DKIM and hosted DMARC is automatic and you never call a "set this TXT record" endpoint at all. That is the point of the hosted model: the API tells you the delegation step, and once it is done the records look after themselves.

Records and verification

For domains you are not fully delegating, or when you want to inspect the live state of records before acting, the API exposes the same checks that power the public tools. The lookup endpoint takes a type that is case-sensitive, and the accepted values are DMARC, SPF, DKIM, MTA-STS, BIMI, MX and TLS-RPT:

  • The lookup endpoint (/api/lookup) returns the live, resolved record for a domain as seen on the public internet, plus the parsed analysis: SPF lookup count against the ten-lookup limit, DKIM selector validity, DMARC policy and alignment mode. This is the programmatic equivalent of running the DMARC checker, SPF checker and DKIM checker by hand.
  • The same checks let you confirm that delegation or a required record is actually live in DNS before you mark a domain as ready in your own system. Do not assume propagation is instant; re-run the lookup until the expected record appears.

The reason to use the lookup endpoint rather than resolving DNS yourself is that it does the recursive SPF expansion and alignment logic for you, so you get the same verdict the dashboard shows rather than a raw record you then have to interpret. If you want to understand what those verdicts mean, the requirements page and the glossary are the reference.

DKIM keys

DKIM is worth a note because of how key material is handled. DMARC Engine generates the key pair in your browser using WebCrypto; the private key is shown to you once and is never sent to or stored by DMARC Engine. You install that private key in your own sending platform, which does the actual signing, and DMARC Engine publishes the matching public key via the CNAME delegation. There is no platform-side signing service.

Because of that, the DKIM create call expects the public key in the body: { selector, keyType, keySize, publicKey }. A PATCH lets you toggle the RFC 6376 t=y testing flag on a selector. Rotation is not automatic: you get a monthly "rotation due" reminder for selectors older than roughly 180 days, and you can run an old and a new selector in parallel so you rotate without an outage. The background is in DKIM key rotation.

Reports and analytics

This is the most common read-only integration: pulling DMARC aggregate report data into your own systems.

  • The reports endpoint (/api/reports) returns parsed aggregate (RUA) data for a domain over a date range: volume, pass and fail counts by source IP and by sending service, with SPF and DKIM results and alignment broken out. This is the data behind reading your first DMARC report, already parsed from the raw XML so you do not have to write a parser.
  • The same data lets you summarise the distinct sending sources DMARC Engine has identified for a domain, which is what you watch to catch unknown or unexpected senders appearing.

The analytics surface mirrors what the analytics product shows in the dashboard, so if you can see it on a chart there, you can pull it over the API and put it on your own chart, in your own data warehouse, or into your own anomaly detection. If you would rather upload an individual report and see it parsed without an integration, the DMARC report analyzer does that interactively.

Pull reports on a schedule (daily is plenty for most domains; aggregate reports arrive roughly once a day from each receiver) and store the results keyed by domain ID and date. Do not re-fetch the entire history every run; fetch the new window and append.

Hearing about changes: notification channels and polling

There are no inbound webhooks to subscribe to. Instead, DMARC Engine pushes alerts out to notification channels that you configure in the dashboard under Settings -> Notifications, and for anything you want to drive from code you poll the REST endpoints.

The channels you can wire up are:

  • Email
  • Slack
  • A generic HTTPS webhook (DMARC Engine POSTs to a URL you control)
  • PagerDuty

These are the same change-detection signals that drive the emailed monitoring and alerts, routed to wherever your team already watches. The kinds of change worth routing somewhere visible:

  • A new sending source appeared. DMARC reports show a source IP or service sending as your domain that we have not seen before. This is the single most useful alert, because a new legitimate sender means "go authenticate it before you tighten policy", and a new illegitimate sender means "someone may be spoofing you". Either way you want to know within hours, not at the next manual review. The background to why this matters is in why your domain can be spoofed.
  • Authentication started failing. A previously passing source begins failing SPF or DKIM, or alignment breaks. Common causes are a vendor rotating their sending IPs out from under an old flattened SPF record, a DKIM key rotation gone wrong, or forwarding breaking alignment.
  • A managed record changed. When the hosted system updates a record on your behalf (for example, re-flattening SPF after a vendor changed IPs), the alert gives you a record for your change log.
  • Policy or posture changed. The domain moved from p=none to quarantine or reject, or a verification completed.

For automation that needs to act on these conditions (rather than just notify a human), poll the REST endpoints on a sensible cadence: read the reports for new sources and rising fail rates, and read the domain state for posture changes. The Slack and PagerDuty channels handle "tell a person now"; the polling loop handles "make my system react". Reading reports and domain state over the API is also your backstop if a notification is ever missed.

Worked examples

These use curl for clarity; translate the same requests into whatever HTTP client your language gives you. In every case the key lives in an environment variable, never inline. The IDs and response fields shown are illustrative; confirm exact shapes against your own responses.

Add a domain during onboarding

curl -sS -X POST https://app.dmarcengine.com/api/domains \
  -H "x-api-key: $DMARC_ENGINE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "client-example.com"}'

A successful create returns the domain object, including the ID you should store and the delegation step still outstanding (the exact shape is illustrative here):

{
  "id": "<domain-id>",
  "domain": "client-example.com",
  "policy": "none",
  "delegated": false,
  "next_steps": [
    {
      "type": "dns_delegation",
      "description": "Point NS records for the managed zone to DMARC Engine"
    }
  ]
}

Check live authentication state

curl -sS "https://app.dmarcengine.com/api/lookup?domain=client-example.com&type=DMARC" \
  -H "x-api-key: $DMARC_ENGINE_KEY"

Remember the type is case-sensitive: DMARC, SPF, DKIM, MTA-STS, BIMI, MX or TLS-RPT. This returns the resolved record and the parsed verdicts: the SPF lookup count, whether DKIM selectors validate, the DMARC policy, and the alignment mode. Use it to gate a "domain is ready" status in your own UI rather than guessing from raw DNS.

Move the policy forward, safely

You should only tighten policy once your reports show legitimate mail is fully authenticated and aligned, which is the whole discipline of going from p=none to enforcement without breakage. When your own checks confirm that, the change is one call:

curl -sS -X PATCH https://app.dmarcengine.com/api/domains/<domain-id> \
  -H "x-api-key: $DMARC_ENGINE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dmarc_policy": "quarantine"}'

A sensible automation reads recent reports first, confirms the fail rate for legitimate sources is at or near zero, and only then issues the PATCH. Do not script a blind jump to p=reject on a timer; alignment problems hide in the long tail of low-volume senders, and the reports are how you find them before they bite.

Pull yesterday's reports for export

curl -sS "https://app.dmarcengine.com/api/reports?domain=client-example.com&from=2026-06-21&to=2026-06-22" \
  -H "x-api-key: $DMARC_ENGINE_KEY"

The response is parsed aggregate data, grouped by source, with pass/fail and alignment per source. Append it to your warehouse keyed by domain ID and date. Run it daily on a cron; do not re-pull history every time.

Poll for new sources

Without inbound webhooks, the way your system learns about a new sending source is to pull reports on a schedule and diff the source list against what you have already stored:

curl -sS "https://app.dmarcengine.com/api/reports?domain=client-example.com&from=2026-06-22&to=2026-06-23" \
  -H "x-api-key: $DMARC_ENGINE_KEY"

Compare the sources in the response against your stored set; anything new is a source to investigate before you tighten policy. For a human-facing alert at the same time, configure the Slack or PagerDuty channel under Settings -> Notifications so someone is told the moment a new source shows up, while your polling job handles the automated reaction.

A realistic end-to-end flow

Putting the pieces together, here is the shape of an MSP onboarding integration that most of this enables:

  1. A client signs up in your portal. Your backend calls POST /api/domains with their domain and stores the returned domain ID against the client record.
  2. Your UI shows the delegation step from the response, with copy-paste instructions. The client (or you, if you manage their DNS) completes delegation.
  3. Your portal polls GET /api/domains/{id} (or re-runs /api/lookup) until delegation reads as live, then flips the client to "active".
  4. Once active, hosted SPF, DKIM and DMARC are managed automatically. You configure the Slack and PagerDuty notification channels so your support team is alerted to new senders and breakage, and your polling job watches the reports so your system can react too.
  5. A daily job pulls reports for every client domain via /api/reports into your warehouse, so your client-facing dashboards and monthly summaries are your own data, refreshed automatically.
  6. When a client's reports show clean alignment, your tooling (or an analyst with a button) issues the PATCH to advance policy toward p=reject.

Nobody logs into a DNS console for routine work. The exceptions surface as notifications, and the steady state is a quiet pipeline.

Practical guardrails

A few things that save pain once you are live:

  • Do not hammer the API in a tight loop. The most common reason a new integration falls over is firing one request per domain across a thousand domains as fast as it can. Spread the work out and add backoff on errors.
  • Store IDs, reconcile by ID. Names change, IDs do not. Reconcile your inventory against GET /api/domains periodically so a domain deleted in one place does not become a ghost in the other.
  • Keep keys narrow and rotate them. Re-read the authentication section. Most real incidents are not clever attacks, they are a broad key in a place it should never have been. Remember a key value is shown only once at creation, so capture it into your secret manager immediately.
  • Use the public tools to sanity-check. When an API verdict surprises you, run the same domain through the matching tool in your browser, DMARC checker, SPF checker, DKIM checker, MTA-STS checker or BIMI checker, to confirm you are reading the same reality the API is.

Where to go next

If you are setting up an integration, start by creating a key and pulling reports, because that is the lowest-risk way to confirm your auth and parsing work before you let automation make changes. Once that is solid, add domain creation and the notification channels, and only then wire up policy changes, with a human in the loop until you trust the alignment data.

For the concepts underneath the calls, the requirements page covers what passing actually means, the glossary defines every term the API returns, and the DMARC, SPF and DKIM product pages explain what the hosted system is doing on your behalf so that your automation has less to do in the first place. Where a particular API detail is not documented here, it is available on request from the team. If you would rather not build any of this and just want domains taken safely to enforcement, that is what the hosted, done-for-you model is for, and the API is there for when you genuinely need to drive it at scale.

Share

See where your domain stands today

Run a free DMARC scan, then let us take you to enforced p=reject with no email outage.