Skip to content

SPF, DKIM and DMARC: what each record does and why your mail needs all three

Three DNS records decide whether a mailbox provider treats your email as authentic. Here is what each one proves, how to set them up on a subdomain, and how to read a failure.

Resend5 min readships at docs/solutions/resend/domain-verification-spf-dkim-dmarc.md

Tags: resend · email · dns · spf · dkim · dmarc · deliverability · domain-verification

You added a sending domain, copied two DNS records, and your first email landed in spam anyway. Or Resend returns a 403 and refuses to send at all. Or Gmail delivers it and Outlook does not.

All three symptoms come from the same place: email has no built-in authentication, so three separate DNS records were bolted on over twenty years to answer the question "is this sender allowed to use this domain". Mailbox providers now check all three, and Google and Yahoo have required them for bulk senders since 2024.

What each record proves

SPF: which servers may send for this domain. A TXT record listing authorised senders:

v=spf1 include:amazonses.com ~all

The receiving server compares the connecting IP against that list. Two traps. First, SPF checks the envelope sender, not the From: header a human sees, which is why SPF alone cannot stop spoofing. Second, there is a hard limit of ten DNS lookups; every include: costs at least one, and a domain with Google Workspace plus a CRM plus a marketing tool plus a transactional provider blows through it. Past ten, the check returns permerror and fails.

DKIM: this message was not tampered with, and the domain owner signed it. Your provider signs each message with a private key; the public key sits in DNS at <selector>._domainkey.yourdomain.com. The receiver fetches it and verifies. DKIM survives forwarding, which SPF does not: a forwarded message keeps its signature but arrives from the forwarder's IP.

DMARC: what to do when the first two fail, and where to send reports. A TXT record at _dmarc.yourdomain.com:

v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; pct=100

DMARC adds the piece SPF and DKIM lack: alignment. It requires that the domain that passed SPF or DKIM matches the domain in the visible From: header. That is what actually stops someone sending as you.

p= is the instruction to receivers: none (monitor only), quarantine (spam folder), reject (refuse outright).

Set it up on a subdomain

Use mail.yourdomain.com, not yourdomain.com.

Reputation is tracked per domain and per subdomain. Keeping transactional mail on its own subdomain means a bad week (a bounce spike, a spam complaint from one campaign) cannot damage the domain your team sends sales mail from, and a compromised marketing tool cannot damage your password resets.

In Resend: Domains → Add domain → mail.yourdomain.com. It shows you the records. Add them at your DNS provider exactly as given, with two gotchas that account for most failed verifications:

  • Do not double the domain suffix. Many DNS UIs append the zone automatically, so entering resend._domainkey.mail.yourdomain.com creates resend._domainkey.mail.yourdomain.com.yourdomain.com. Enter the host part only.
  • Turn off proxying. On Cloudflare, DNS records for mail must be "DNS only" (grey cloud), not proxied.

Verification is usually minutes. Propagation can be an hour. Until the domain shows Verified, Resend rejects sends with a 403: this is not a filtering problem, and no amount of copy tuning fixes it.

Start DMARC at p=none

Publish p=none first and read the aggregate reports for a couple of weeks. They arrive as XML at the rua= address; a free parser is enough. What you are looking for is any legitimate mail failing alignment: a helpdesk, an invoicing tool, a CRM, something a colleague set up two years ago.

Then tighten to p=quarantine, then p=reject. Going straight to reject before you know what sends as you is how a company discovers on a Monday that its invoices have been bouncing since Friday.

Reading a failure

Open the message in Gmail and choose Show original. The header block tells you everything:

SPF:   PASS with IP 54.240.x.x
DKIM:  PASS with domain mail.yourdomain.com
DMARC: PASS
  • SPF fail, DKIM pass, DMARC pass: fine. Common after forwarding, and the reason DKIM matters more than SPF.
  • DKIM fail: the record is wrong, missing, or the message was modified in transit (some mailing lists rewrite bodies). Re-check the selector record.
  • DMARC fail with both passing: an alignment problem. Something is sending with a From: on your root domain while authenticating as a different one.
  • SPF permerror: you are past ten DNS lookups. Flatten the record or move senders onto subdomains.

The verify check

bun run verify asserts that the domain in EMAIL_FROM exists on the Resend account and reads verified. It is deliberately part of the standard check because "we forgot to verify the production domain" is a launch-day failure that looks like a code bug.

One caveat worth knowing before it surprises you: listing domains needs a full-access key, and the onboarding tells you to create a sending-access key, which is the right advice. With such a key Resend answers restricted_api_key and the check passes with a note saying the domain state was not read: confirm it in the dashboard instead. Any other rejection still fails the check, so a wrong key and a deliberately restricted one do not read the same.

Two more things worth doing

Do not send from a no-reply address. Replies to a black hole correlate with complaints, and someone will reply. REPLY_TO exists for this.

Never put a customer's address in From:. "Sam invited you" as a display name is fine; sam@theircompany.com as the actual From fails DMARC on their domain, lands in spam, and is technically indistinguishable from spoofing. Set replyTo to the inviter instead. agent/skills/add-email-template.md builds an invite template as its worked example and does exactly this at the call site.