Technical

SPF, DKIM, and DMARC for Developers Who Just Want Their App's Email to Land in the Inbox

A practical, opinionated walkthrough of the three DNS records your app needs to send transactional or product email that doesn't land in spam. Written for developers who would rather ship than read RFCs.

Sohail HussainSohail Hussain(Updated: )11 min read

If you build software, three DNS records decide whether your password resets, receipts, and magic-link emails land in the inbox or the spam folder: SPF, DKIM, and DMARC. Set all three correctly, point them at whatever SMTP provider you use, and Gmail and Outlook will trust you. Skip any one and modern inbox providers treat your app's mail as suspicious by default.

This is the version I wish I had the first time I shipped an app that sent email. What each record does, what to paste into your DNS, how to verify it, and what to look at when Gmail says dmarc=fail and your CFO's onboarding email has vanished.

Google's 2024 bulk sender rules (Google's official guidance) turned this from nice-to-have into a hard requirement for any app sending more than 5,000 messages a day to Gmail. Yahoo enforces the same rules (Yahoo Sender Hub) and Microsoft followed in May 2025 (Microsoft 365 sender requirements). If you are below that threshold today you will not stay there, and unauthenticated mail is the first thing modern filters quarantine.

The 60-second version

You need three DNS records on the domain in your From: address:

  1. An SPF TXT record listing which servers are allowed to send for your domain.
  2. A DKIM TXT (or CNAME) record containing a public key your provider uses to cryptographically sign every outgoing message.
  3. A DMARC TXT record telling receivers what to do when SPF or DKIM fails, plus where to send reports.

Most managed email APIs generate the exact values for you. Your job as a developer is to understand what each record does so you can debug it when it breaks, and to make sure the alignment between Return-Path, DKIM d=, and From: is correct so DMARC actually passes.

If you remember one thing from this post, remember that DMARC requires alignment rather than mere authentication. A passing SPF check and a passing DKIM check are not enough; the domain in From: has to match, or be a subdomain of, the domain that SPF or DKIM authenticated. This is the most common reason transactional mail fails DMARC, and it stays invisible as long as you only ever ask "did SPF pass?".

Why your app needs this at all

SMTP, the protocol your email rides on, was defined in RFC 5321 and traces back to RFC 821 from 1982. It has no authentication. None. Anyone can connect to a mail server, announce MAIL FROM:<ceo@stripe.com>, and the protocol will not stop them. The entire authentication stack is a retrofit built on DNS.

Inbox providers cannot tell whether the message claiming to come from noreply@yourapp.com was sent by your servers or by a phisher unless your DNS says so. Without these records, two things happen. Receivers downgrade your reputation, and Gmail's Postmaster Tools documentation lists low or absent authentication as a top reason for spam folder placement. Your domain also becomes spoofable, so anyone can phish as your brand and your real customers report your mail as spam, which drives sender reputation further down.

Publishing a DMARC record at p=none and stopping there is extremely common. It looks like security on an audit checklist and does nothing at all in production. Do not be in that group.

SPF: telling the world which servers can send for you

SPF (Sender Policy Framework, defined in RFC 7208) is a single DNS TXT record listing which IPs and hostnames may send mail using your domain in the SMTP envelope.

v=spf1 include:_spf.google.com include:amazonses.com include:spf.mailneo.co -all

Read left to right: this domain authorizes Google Workspace, Amazon SES, and Mailneo. Anyone else gets hardfailed by -all.

If you use a managed provider, copy their include: from their docs and stop there. Do not over-engineer it. The -all hardfail is deliberate; ~all softfail gives spammers wiggle room, and some receivers discount softfails for alignment purposes anyway.

Three things reliably bite developers here.

The 10 lookup limit is real. SPF allows a maximum of ten DNS lookups per evaluation and every include: counts against it. Stack Google, SendGrid, Mailgun, a CRM, and a help desk and you blow the ceiling; the whole check then returns permerror, which receivers treat as a fail. If you are at ten, stop adding includes and consolidate.

SPF authenticates the Return-Path, not the From: header, and this is the headline trap. If your app calls SES with MAIL FROM: bounces@yourapp.com while your From: header says hello@yourapp.com, SPF passes for yourapp.com and aligns fine. But a shared service that sets Return-Path: bounces@ses-sender.com passes SPF for a domain that has nothing to do with yours, so DMARC fails alignment even though SPF technically passed.

SPF also breaks on forwarding. When alice@yahoo.com forwards your message to alice@gmail.com, Yahoo's server is the one talking to Gmail, and Yahoo is not in your SPF record. That is the entire reason DKIM exists.

DKIM: signing every message with a private key

DKIM (DomainKeys Identified Mail, RFC 6376) is a cryptographic signature your sending server adds to every outgoing message. The receiver pulls your public key from DNS, verifies the signature, and learns two things: the message body was not altered in transit, and the signing domain in the d= tag really controls that DNS record.

The record lives at selector._domainkey.yourdomain.com. The selector lets you rotate keys without downtime; your provider picks one.

selector1._domainkey.yourapp.com.   TXT   "v=DKIM1; k=rsa; p=MIGfMA0GCSq..."

Some providers prefer CNAMEs pointing at keys they host, which is genuinely better because rotation stops being your problem and you never paste a 2048-bit key into a DNS UI.

The failure modes are boring and common. Teams enable DKIM on the transactional API and forget the marketing tool, the CRM, the help desk, and the invoicing tool; every stream needs DKIM signed by a domain that aligns with its From:. Keys generated before 2017 and never rotated are often too short, since anything below 1024 bits reads as broken and 2048 is the de facto minimum now (Google's sender guidance). And some DNS providers split long TXT records across multiple strings, which is valid but which a small number of MTAs mis-concatenate; if DKIM fails intermittently, check the raw record with dig +short TXT selector._domainkey.yourapp.com and confirm it comes back as one continuous string.

The mental model that keeps it straight: SPF authenticates the path, DKIM authenticates the content, and DMARC demands that one of them passes and aligns with the visible From: domain.

DMARC: the policy that ties it together

DMARC (Domain-based Message Authentication, Reporting, and Conformance, RFC 7489) is the rulebook. It lives at _dmarc.yourdomain.com and tells receivers what policy to apply when SPF or DKIM fails (none, quarantine, or reject), what percentage of failing mail to apply it to (pct=), where to send aggregate XML reports (rua=), where to send per-message forensic reports (ruf=, rarely used now), and how strict to be about alignment (aspf=, adkim=).

v=DMARC1; p=none; rua=mailto:dmarc@yourapp.com; fo=1;

p=none means monitor only, do not change delivery. Always start here. Read the aggregate reports for two to four weeks, confirm every legitimate sender passes, then ratchet up to p=quarantine; pct=25 and eventually p=reject; pct=100.

Going straight to p=reject is the mistake that generates the support ticket. The hard part of DMARC is finding every system that sends as your domain, and almost every team has at least one shadow sender: a hiring tool, a calendar app, the founder's mailmerge script. Jumping to reject before you have visibility blocks legitimate mail, usually somebody important's.

The other three traps are quieter. Not reading the aggregate reports at all, which makes DMARC a guess, since the raw XML is unreadable by hand and needs a parser. Confusing relaxed with strict alignment, where aspf=r (the default) lets a subdomain authenticate for the parent so bounces.yourapp.com aligns with yourapp.com, while aspf=s demands an exact match and is rarely what you want. And assuming subdomains need their own records; a record at _dmarc.yourapp.com covers subdomains by default through the sp= tag, and you only publish _dmarc.mail.yourapp.com when you deliberately want a different policy there.

A working setup, end to end

Here is a complete setup for an app called acme.app sending transactional mail through Amazon SES, marketing email through Mailneo, and the team's regular mail through Google Workspace.

;; SPF: one record, three senders
acme.app.                  TXT   "v=spf1 include:amazonses.com include:spf.mailneo.co include:_spf.google.com -all"

;; DKIM: one selector per provider, usually as CNAMEs
selector1._domainkey.acme.app.   CNAME   selector1.acme.app.dkim.amazonses.com.
selector2._domainkey.acme.app.   CNAME   selector2.acme.app.dkim.amazonses.com.
mn1._domainkey.acme.app.         CNAME   mn1.acme.app._domainkey.mailneo.co.
google._domainkey.acme.app.      TXT     "v=DKIM1; k=rsa; p=MIGfMA0G..."

;; DMARC: start at none, work up to reject
_dmarc.acme.app.           TXT   "v=DMARC1; p=none; rua=mailto:dmarc-reports@acme.app; fo=1; adkim=r; aspf=r;"

After a couple of weeks at p=none, with reports confirming every legitimate stream passes both SPF and DKIM with alignment, tighten it:

_dmarc.acme.app.           TXT   "v=DMARC1; p=quarantine; pct=50; rua=mailto:dmarc-reports@acme.app; fo=1; adkim=r; aspf=r;"

Then a few more weeks at p=quarantine with pct=100, and finally:

_dmarc.acme.app.           TXT   "v=DMARC1; p=reject; rua=mailto:dmarc-reports@acme.app; fo=1; adkim=r; aspf=r;"

The whole ramp takes about a month if your sending is straightforward and three months if you have a decade of vendor sprawl to excavate. For a step-by-step version with DNS screenshots, see the SPF, DKIM, and DMARC setup guide.

Verifying that it actually works

Check the records exist:

dig +short TXT acme.app
dig +short TXT selector1._domainkey.acme.app
dig +short TXT _dmarc.acme.app

Then send a real message to a Gmail address and read the headers. Open the message, click Show original, and look for the authentication results block:

ARC-Authentication-Results: i=1; mx.google.com;
  dkim=pass header.i=@acme.app header.s=selector1 header.b=...;
  spf=pass (google.com: domain of bounces+...@amazonses.com) smtp.mailfrom=bounces+...@amazonses.com;
  dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=acme.app

Three things matter there. dkim=pass with the domain after header.i=@ matching, or being a subdomain of, your From: domain. spf=pass is nice, but for DMARC what you need is header.from=acme.app and an authenticating domain that aligns with it. And dmarc=pass, which is the bottom line; if it says dmarc=fail, the SPF and DKIM lines above it will tell you which one authenticated and which one failed alignment.

Port25's verifier at check-auth@verifier.port25.com is the canonical second opinion. Send it a message and it replies with a full report covering SPF, DKIM, DMARC, and SpamAssassin scoring, free and without a signup.

For ongoing work, our email header analyzer parses raw headers and explains each line in plain English, and the understanding email headers guide covers what the fields mean. If authentication is clean and mail still misbehaves, the problem has moved downstream to reputation and content; check inbox placement against delivery rate and start on the email deliverability guide.

The three failures I see most often

SPF passes, DKIM passes, DMARC fails. This is nearly always alignment: the provider is signing with their domain instead of yours. Check the d= tag in the DKIM-Signature header, and if it reads d=ses.amazonaws.com rather than d=acme.app, you skipped domain verification in the provider console and it fell back to its own key. Finish that step and DKIM signs as your domain.

Mail from one specific feature lands in spam while everything else is fine. Nearly always an unauthenticated shadow sender: a help desk, a hiring system, somebody's personal marketing account. Pull the headers from one of the spammed messages, read the Authentication-Results line to identify the real sender, then either add it to SPF, set up DKIM for it, or move it to a subdomain so it stops threatening your main domain's reputation.

DKIM passes for some messages and fails for others. Usually content modification. A mailing list, a forwarding rule, or an antivirus appliance rewrites the body before forwarding and invalidates the signature. There is nothing to fix in DNS; make sure SPF aligns as a fallback, and use ARC if you are the one doing the forwarding.

SPF record count and DNS propagation

Can I have more than one SPF record?

No. RFC 7208 forbids multiple v=spf1 TXT records on a single domain; receivers return permerror and treat your SPF as broken. To authorize several senders, combine them into one record with multiple include: mechanisms.

How long do DNS changes take to propagate?

Most modern providers propagate in under a minute. Older ones can take up to your record's TTL, often an hour. Plan a one-hour window after publishing before you start testing, otherwise you will debug a record that simply has not landed yet.

What about BIMI

BIMI (Brand Indicators for Message Identification) is the fourth standard and worth a short note. It displays your logo beside your messages in supported inboxes, and it requires DMARC at p=quarantine or p=reject. At p=none it does nothing at all. Fix DMARC first; BIMI is the reward for finishing.

spfdkimdmarcdeliverabilitydeveloperstransactional-emaildns
Share this article
Sohail Hussain

Sohail Hussain

Founder & CEO at Mailneo

Building Mailneo — AI-powered email marketing for growing businesses.

Ready to supercharge your email marketing?

Start sending smarter emails with AI-powered campaigns. No credit card required.

Get Started Free