SPF, DKIM and DMARC check

Tell every customer which DNS record is wrong

Your customers publish DNS for their own domains, and some of it goes wrong. Diagnostics reads their zone live and returns each problem as a finding: which record, what is in its way, and the exact change that fixes it.

Verify says which record.

POST /domains/dom_…/verifyverified_at: null
  • DKIMTXT aibx._domainkey.mail.oakline.compending
  • RETURN_PATHMX bounces.mail.oakline.comverified
  • SPFTXT bounces.mail.oakline.comverified
  • DMARCTXT _dmarc.mail.oakline.comverified
  • INBOUNDMX mail.oakline.comverified

Diagnostics says why, and what to change.

GET /domains/dom_…/diagnosticsbreaking
id
record-doubled:DKIM
title
The DKIM record ended up at the wrong name
detail
The value is published — but at aibx._domainkey.mail.oakline.com.oakline.com instead of aibx._domainkey.mail.oakline.com, so nothing finds it.
fix
Edit that record and set its name to just "aibx._domainkey.mail", without "oakline.com" on the end.
evidence
aibx._domainkey.mail.oakline.com.oakline.com TXT v=DKIM1; k=rsa; p=MIIBIjAN…

Live lookups

Read from their nameservers, not a cache

A resolver keeps serving an old answer until its TTL runs out, which is exactly when a customer has just fixed it. Lookups ask the zone's own nameservers first, and fall back to an ordinary resolver only when those have no answer or cannot be reached. Nothing is stored, so every call reads the zone again.

Live lookups
TXT_dmarc.mail.oakline.com
A caching resolvernot read
answer kept until its TTL runs out
  • v=DMARC1; p=none
  • v=DMARC1; p=none; rua=mailto:dmarc@mail.oakline.com
2 DMARC records
The zone's own nameserverread
what the customer just saved
  • v=DMARC1; p=none; rua=mailto:dmarc@mail.oakline.com
1 DMARC record

Findings

Worst first, with the fix attached

Breaking means mail fails. Risk means it works today and will cost mail later. Tip means nothing is wrong. Each finding names the record it concerns, when there is one, and quotes the lines it was read from.

Findings
northpier.io3 findings
  1. breakingMail sent from this domain is being rejected right nowdmarc-enforced-early
  2. riskThe SPF record on northpier.io asks for too much workspf-lookups:northpier.io
  3. tipNobody is collecting this domain's DMARC reportsdmarc-no-reports

Events

Verified once, and watched after

domain.verified fires when the DKIM key verifies. Domains are rechecked in the background, so one whose key disappears loses verified_at, sends domain.lost, and comes back by itself once the record does.

Events
mail.oakline.com
  1. domain.verified14 Sep, 09:52DKIM found at the right name
  2. domain.lost3 Feb, 16:08Key deleted in a DNS clean-up; sends from it fail with domain_unverified
  3. domain.verified3 Feb, 16:40Record put back, verified on the next check

Records

The records, in the shape a DNS panel asks for

Each record comes with its purpose, type, name, value and TTL. In the console they also download as a zone file of what is still outstanding, marking any record the customer has to delete first, since an import only adds.

Records
mail.oakline.com.zone2 records outstanding
; replaces 10 mx.oldhost.net — delete that record firstmail.oakline.com. 3600 IN MX 10 inbound-smtp.eu-central-1.amazonaws.com.aibx._domainkey.mail.oakline.com. 3600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…"

Every rule

Why a customer's domain won't verify

Most failures are not a missing record but a record in the wrong shape, or something else in the zone standing in its way. These are the rules diagnostics runs, and the id each finding comes back with.

Some customers never need to touch DNS at all: an address under your own wildcard domain is live the moment it is created.

Any record

  • Published at the name with the domain on it twice, because the DNS panel appended the zone to a full namerecord-doubled:<purpose>breaking
  • The right value with quotation marks stored around it, so it no longer matches character for charactervalue-mangled:<purpose>breaking
  • A CNAME already sits at the name, and a name with an alias can hold nothing elsecname-conflict:<purpose>breaking

DKIM

  • The domain's DMARC already says reject or quarantine while the key is not published, so mail is refused or filed as spam until it isdmarc-enforced-earlybreaking

SPF

  • Two SPF records on one name: receivers give up on both. Breaking on the return path, a risk on the root domainspf-multiple:<name>breaking
  • More than ten DNS lookups in the record's own mechanisms, before counting what each include pulls inspf-lookups:<name>risk
  • The record ends in +all, which lets any server send as the domainspf-open:<name>risk
  • DMARC set to aspf=s, which refuses the bounces subdomain; mail still passes on DKIM alonedmarc-strict-spfrisk

DMARC

  • More than one DMARC record, so receivers ignore all of themdmarc-multiplebreaking
  • No p= tag, so the whole record is thrown awaydmarc-invalidrisk
  • No rua address, so nobody receives the aggregate reportsdmarc-no-reportstip

MX, for receiving

  • Another MX at the same or a lower priority number, so mail goes there however green our record readsmx-precedencebreaking
  • The MX points at our inbound in a different region from the one the domain is set up inmx-other-regionbreaking
  • An MTA-STS policy in enforce mode that does not list our server. In testing mode it is a tipmta-sts-mismatchbreaking
  • A wildcard CNAME at *.domain, where a wildcard's MX has to gowildcard-cnamebreaking
  • Another provider receives the domain's mail today, and publishing our MX moves itmx-foreignrisk
  • An MTA-STS record announces a policy file that nothing servesmta-sts-unreachablerisk
  • Old mail servers left behind ours as backupsmx-backuptip

The zone

  • No nameservers answer for the domain, so it is unregistered, expired or only just delegatedzone-missingbreaking
  • A * record answers for names nobody set up, so a missing record can read as a wrong onewildcard-zonerisk
  • Under a wildcard, the apex keeps receiving wherever it does todayapex-keeps-mailtip
  • Some lookups did not answer in time, so the list may be shortlookups-incompletetip

Set up

A domain check in your onboarding flow

1domains.createthe records to show the customer
const domain = await aiinbx.domains.create({  name: "mail.oakline.com",}) for (const record of domain.records ?? []) {  show(record.purpose, record.type, record.name, record.value)}
2domains.verifywhen they press Check
const domain = await aiinbx.domains.verify(domainId) if (!domain.verified_at) {  const { data } = await aiinbx.domains.diagnostics(domainId)  return data.map(({ severity, title, fix }) => ({ severity, title, fix }))}
3your webhook handlerlive, and when it stops being
if (event.type === "domain.verified") {  await customers.markSendingReady(event.data.domain_id)}if (event.type === "domain.lost") {  await customers.askToRestoreDns(event.data.domain_id)}

Questions

Why is my domain not verifying?

Usually because a record is published in the wrong shape rather than not at all: at a name with the domain on it twice, with quotation marks stored in the value, next to a CNAME, or beside a second SPF or DMARC record. The diagnostics endpoint looks for each of these in the live zone and returns the record it found and the change to make.

Can a domain have two SPF records?

No. A name may hold one SPF record, and a receiver that finds two treats the check as failed rather than combining them. Keep one, move the include: parts of the other into it, and delete the extra. Diagnostics reports it as spf-multiple, with both records quoted.

Do I have to change the SPF record on my root domain?

No. The SPF record AI Inbx asks for goes on the return-path subdomain, bounces.<domain>, so the root domain's SPF stays as it is. Diagnostics still reads it and flags a duplicate, more than ten lookups or +all as a risk, because those break the check for everything else that sends as the domain.

Why does a DNS checker show my record while verification says it is missing?

Either the checker is reading a cached answer, or it found the value somewhere other than the exact name that is looked up. AI Inbx asks the zone's own nameservers first, so a record corrected a minute ago reads as it is now, and a value buried one zone deeper is reported as record-doubled.

How long does DNS verification take?

Usually minutes and occasionally hours, depending on the DNS host. A record not found yet reads pending for 48 hours after the domain is added, and missing after that. Rather than polling POST /domains/{id}/verify in a tight loop, subscribe to the domain.verified webhook, which fires when the DKIM key verifies.

Does a verified domain mean SPF and DMARC are correct?

No. verified_at is set when the DKIM key verifies, which is what sending needs. It does not certify SPF, DMARC or the inbound MX record; each record carries its own state, and diagnostics reports what is wrong with the rest of the zone.

Give your product an inbox. The hard parts are already handled.

Get started