Skip to content

Verify a sending domain, and read the verification screen honestly

How verification checks each record over DoH, what the four states mean, and why a domain can sit at pending for an hour without anything being wrong.

DeliverabilityBeginner10 min readUpdated

01How verification checks a record

Verification is not a scan, a crawl, or a queue. It is one DNS-over-HTTPS query per required record, made against Cloudflare’s public resolver at the moment you press the button, and then a comparison. That is the whole mechanism, and knowing it is the whole mechanism is what lets you reason about a result instead of retrying it.

ROW
name + type
one record your transport asked for
DoH
Cloudflare public resolver
one live query, at the moment you pressed the button
NORMALISE
join, unquote, strip
TXT chunks rejoined; MX exchange taken off the raw answer
MATCH
exact · include · prefix
per-row match mode
STATE
verified · pending · failed · error

The wire format is why normalising comes first. TXT answers come back quoted and chunked at 255 bytes, because that is the wire format and a 2048-bit DKIM key does not fit in one string; the checker joins the chunks and unquotes them before comparing anything, which is why a key that looks split across three quoted pieces in your zone file still matches. MX answers arrive as <priority> <exchange>, so the exchange is taken off the end of the raw answer rather than out of the normalised form, where the space it was split on no longer exists.

Match modeUsed forWhat it demands
exactEverything not listed belowThe published value must equal the expected value. Whitespace is stripped, a trailing dot is stripped, and the comparison is case-insensitive — so a zone file that qualifies a name with a final dot still matches. Everything else has to be identical.
includeSPF, where merging is the correct answerA domain may publish exactly one SPF record, so a reader who already has one is right to add our include to it rather than publish a second. The check pulls every include: token out of the expected value and requires all of them to appear in what resolved.
prefixA value only the transport knowsA DKIM key the provider mints, or a DMARC policy you are free to tighten. The expected value is truncated at the first semicolon and the answer only has to start with it — v=DKIM1 matches whatever key follows.

One more filter: when the expected value starts with v=spf1, only published records that also start with v=spf1 are counted as relevant, so the found field shows that filtered set joined rather than your whole apex TXT collection.

02verified, pending, failed, error

A record check returns one of four values, not a boolean. That is a deliberate cost — four states is more UI, more copy and more branching than a checkmark — and it is paid because “we could not reach a resolver” and “the record says the wrong thing” are different problems with different fixes, and collapsing them into not verified sends you to your DNS panel to fix something that is not broken.

TL;DR

pending means nothing is published at that name; failed means something is published and disagrees; error means nobody managed to look. Only one of the three is worth opening your DNS panel for.

StateWhat happenedWhat follows from it
verifiedThe lookup succeeded and the answer matched.Nothing to do. The domain row records when this last happened, and the send path drops its cached copy of the domain immediately, so a domain that just became verified is sendable now rather than in five minutes.
pendingThe lookup succeeded and there was nothing there.An empty answer set — or NXDOMAIN, which is a real answer meaning the name does not exist. Either you have not published it yet, or you published it and a resolver is still serving the old cached negative. Wait, or check from a second resolver.
failedThe lookup succeeded and the answer disagreed.The only actionable state, and the response carries what actually resolved so you can diff it against what was asked for. Nine times out of ten the difference is smaller than you expect.
errorThe lookup did not complete.SERVFAIL, REFUSED, a resolver returning a non-200, a query type it will not answer. This says nothing whatsoever about your zone. It is a separate state because folding it into pending makes a resolver outage look like a customer who never published anything.
pending — the name resolved to nothing
  • Propagation, a typo in the name, or a record you have not added
  • Not worth staring at a value for: there is no value
failed — something is published and disagrees
  • A truncated key, a merged SPF record missing our include, a value your registrar helpfully wrapped in extra quotes
  • The screen shows a found-against-expected diff. Read it before you retype the record

Failed rows carry what resolved. It is usually a trailing dot, a doubled zone suffix, or a smart-quote your DNS panel substituted while you were pasting — which is why the diff is worth a second of reading before it is worth a retype.

03How the domain-level status is decided

The domain-level status is not a vote and not an average. It is three lines, and the thing to read off them is what happens to a row nobody could check.

if (statuses.every((s) => s === 'verified')) return 'verified'
if (statuses.includes('failed')) return 'failed'
return 'pending'
When the rows areThe domain isWhich means
all verifiedverifiedEvery record was actually looked up and actually agreed.
any one failedfailedOne published record disagrees. Go and read it.
anything elsependingIncluding every errored row: a row we could not check is not a row that passed. Pending is the state that keeps you checking rather than the state that tells you to stop.
there are nonenot_startedA different statement from pending, and kept separate for that reason.
origin: copy
  • A row that is yours to publish
  • It is in the zone file you export
origin: observe
  • A row the transport publishes for itself — there is nothing for you to paste anywhere
  • The check exists only so the screen can confirm the provider has done its half
  • Every Cloudflare Email Service row is one: the SPF include, the cf-bounce._domainkey DKIM shape, the cf-bounce MX pointing at mx.cloudflare.net, and the DMARC row

So a Cloudflare domain at pending is not a domain where you forgot something. It is a domain where the transport has not finished writing its own records yet, and the fix is on that side. That is why the “write these records to my zone for me” action on a Cloudflare domain answers there is nothing here to write rather than reporting a success it did not perform, and why the exported zone file emits those rows as comments saying who publishes them. To make the distinction visible, the verify call also asks the transport directly what it thinks the identity’s state is, best-effort — a transport that will not answer must not be allowed to fail a verify, so its silence is logged and ignored rather than surfaced as your problem.

Readiness flagHow it is computed
dkim_readyevery rather than some over the matching rows. A domain with two DKIM records where one passes and one fails is not a domain with working DKIM, and reporting it ready is precisely how a half-published key reaches production.
spf_readyThe same every, for the same reason.
dmarc_policyDistinguishes null (nobody has looked) from missing (we looked and it is not there) — the same honest-about-ignorance move as the error state.

These three ride alongside the roll-up rather than inside it, because a single word cannot say which half is missing.

04When a domain will not verify

Four things account for nearly every domain that will not verify, and none of them are a bug in the checker. In rough order of how often they are the answer:

CauseWhy it reads as pendingWhat to do
1. The TTL you setIf a name never existed, a public resolver will find it within seconds. If you changed a record that had a 24-hour TTL, every resolver that already asked is entitled to serve the old answer for up to 24 hours, and no button in this product or anyone else’s revokes that. Negative answers cache too: the SOA minimum controls how long “this name does not exist” is remembered, so a name you just created can read as pending for the length of that timer.Drop the TTL to 300 seconds before the change you know is coming, not after. Otherwise, wait it out.
2. Split-horizon DNSYour laptop resolves through your company’s internal resolver, which serves an internal view of the zone; verification resolves through a public one, which serves the external view. When those two views disagree, dig on your machine and the verification screen contradict each other indefinitely and both are telling the truth.Check with a public resolver explicitly, before you conclude anything.
3. The registrar that appendsMost DNS panels append the zone to the name you type, whether or not you already appended it yourself. Typing cf-bounce._domainkey.yourdomain.com into one produces cf-bounce._domainkey.yourdomain.com.yourdomain.com, which resolves to nothing, reads as pending forever, and looks completely correct in the interface that created it.Some panels want a bare label and some want a fully qualified name with the trailing dot; the only way to know which yours is is to look at what came out. If dig disagrees with your DNS panel, believe dig.
4. CNAME at the apexA name that has a CNAME may have no other records at all — that is the rule, not a limitation of any particular provider — so if your apex is CNAMEd at a host or a site builder, your apex SPF and DMARC records either cannot be created or are being quietly ignored. This is the failure mode that most looks like the checker is broken, because the record is visibly there in the panel.Providers offering ALIAS, ANAME or CNAME flattening synthesise an answer at query time and usually handle it fine. Providers who do not will let you create the TXT record in their UI and then never serve it — move the zone, or move the apex.
dig @1.1.1.1 +short TXT yourdomain.com            ← the view verification sees
dig +short TXT yourdomain.com                     ← the view your machine sees
dig @1.1.1.1 +short MX cf-bounce.yourdomain.com

If rows keep coming back error, look upstream. With all four ruled out and rows still erroring rather than pending or failed, your nameservers are returning SERVFAIL to a public resolver, which usually means a DNSSEC signature that no longer validates. That is a zone-level fault, it breaks far more than mail, and it is worth treating as an incident rather than as a verification problem. When you have a verified domain, the next thing worth doing is moving DMARC off p=none.

What just happened

You can now read the verification screen as evidence rather than as a verdict: each row is one DNS-over-HTTPS lookup, matched by one of three rules, reported in one of four states, and the response tells you how many of the rows were actually resolved. The thing most likely to bite you later is not a wrong value — it is a cached one. Nothing in this product can make a resolver forget an answer it is still entitled to serve, so the honest response to pending is usually to wait out the TTL you set before you edited.

Common questions

Read next

YOUR ACCOUNT, YOUR MAIL

Nothing to sign up for. Just deploy it.

Every guide on this site describes software you run yourself.