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 Hussain14 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 of them and modern inbox providers will treat your app's mail as suspicious by default.

This post is the version I wish I had the first time I shipped an app that sent email. There is no marketing fluff. We will go through what each record does, what to paste into your DNS, how to verify it, and what to look at when Gmail tells you dmarc=fail and your CFO's onboarding email vanishes.

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 per 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.

Table of contents

The 60-second version

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

  1. An SPF TXT record that lists 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 (Postmark, SES, Mailgun, Resend, SendGrid, Mailneo, etc.) generate the exact values for you. Your job as a developer is to (a) understand what each record does so you can debug it when it breaks, and (b) make sure the alignment between Return-Path, DKIM d=, and From: is correct so DMARC actually passes.

If you remember nothing else from this post, remember: DMARC requires alignment, not just authentication. A passing SPF and a passing DKIM check are not enough; the domain in From: has to match (or be a subdomain of) the domain SPF/DKIM authenticated. This is the single most common reason transactional mail fails DMARC, and it is invisible if you only test "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 and say "MAIL FROM:<ceo@stripe.com>" and the protocol will not stop them. The entire authentication stack is a retrofit built on DNS.

That matters for your app because inbox providers cannot tell whether the email claiming to be from noreply@yourapp.com was sent by your servers or by a phisher unless your DNS has SPF, DKIM, and DMARC published. Without them, two things happen:

  • Receivers downgrade your reputation. Gmail's Postmaster Tools documentation explicitly lists "low or no authentication" as a top reason for spam folder placement.
  • Your domain becomes spoofable. Anyone can send phishing mail as your brand and your real customers will be the ones reporting it as spam, which tanks your reputation further.

According to Valimail's 2024 fraud report, only about 33% of domains with DMARC records have it set to enforce. The other two-thirds are publishing records that look like security but do nothing. Do not be in that two-thirds.

SPF: telling the world which servers can send for you

SPF (Sender Policy Framework, defined in RFC 7208) is a single DNS TXT record on your domain that lists which IPs and hostnames are allowed to send mail using your domain in the SMTP envelope.

A typical SPF record looks like this:

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 to send mail. Anyone else (-all) is hardfailed.

What developers get wrong about SPF

The 10 lookup limit is real. SPF allows a maximum of 10 DNS lookups per evaluation. Every include: counts. Add Google + SendGrid + Mailgun + a CRM + a help desk and you blow the limit; the entire SPF check then returns permerror and inbox providers treat it as a fail. The SPF Surveyor by dmarcian will count your lookups for you. If you are at 10, stop adding includes and consolidate.

SPF only authenticates the Return-Path (envelope-from), not the From: header. This is the headline trap. If your app calls SES with MAIL FROM: bounces@yourapp.com but your From: header says hello@yourapp.com, SPF passes for yourapp.com, which usually aligns fine. But if you use a shared service that sets Return-Path: bounces@ses-sender.com, SPF passes for ses-sender.com, which does not align with yourapp.com, and DMARC will fail SPF alignment even though SPF technically passed.

SPF breaks on forwarding. When alice@yahoo.com forwards your message to alice@gmail.com, Yahoo's server is the one talking to Gmail. Yahoo is not in your SPF record. SPF fails. This is why DKIM exists.

What to actually paste

If you use a managed provider, copy their include: from their docs and stop. Do not over-engineer. A correct SPF record for an app sending through SES and Google Workspace is exactly:

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

That is it. The -all (hardfail) is intentional; ~all (softfail) gives spammers wiggle room and some receivers ignore softfails for DMARC alignment purposes.

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 now knows two things:

  1. The message body has not been altered in transit.
  2. The signing domain (in the d= tag of the DKIM-Signature header) really controls that DNS record.

A DKIM record lives at a selector._domainkey.yourdomain.com subdomain. The selector lets you rotate keys without downtime; your provider picks one. The record contains the public key:

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

Some providers (SES, Resend, Mailneo) prefer CNAME records that point at keys they host, which is genuinely better because they handle rotation for you. You do not have to copy/paste a 2048-bit key into your DNS UI.

What developers get wrong about DKIM

Forgetting to enable it on every sending stream. Your transactional API has DKIM. Your marketing tool, your CRM, your help desk, and your invoicing tool may not. Every stream needs DKIM signed by a domain that aligns with your From:.

Using a weak key length. Anything below 1024 bits is treated as broken by Google; 2048 bits is now the de facto minimum. If your DKIM record was generated before 2017 and you have not rotated, rotate now. Google's sender guidance lists 1024-bit minimum.

Letting Cloudflare wrap your DKIM TXT record. Cloudflare DNS occasionally splits long TXT records across multiple strings; that is technically valid per RFC 7208, but a small number of MTAs mis-concatenate. If you are seeing intermittent DKIM failures, check the raw record with dig +short TXT selector._domainkey.yourapp.com and confirm it is one continuous string.

A quick mental model

SPF authenticates the path (which server). DKIM authenticates the content (which domain owns the signature). DMARC requires one of them to pass AND align with the visible From: domain.

DMARC: the policy that ties it all 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 that policy to (pct=).
  • Where to send aggregate XML reports (rua=).
  • Where to send per-message forensic reports (ruf=, rarely used now).
  • How strict to be about alignment (aspf=, adkim=).

A starter DMARC record looks like this:

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

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

What developers get wrong about DMARC

Going straight to p=reject. The reason DMARC is hard is that you have to find every system sending mail as your domain — and almost every team has at least one shadow sender (a hiring tool, a calendar app, the founder's mailmerge-from-Sheets script). Going to reject before you have visibility will block legitimate mail. Always crawl through nonequarantinereject.

Not reading the aggregate reports. The XML is unreadable by hand. Use Postmark's free DMARC monitoring, dmarcian, or roll your own parser. Without reports, DMARC is a guess.

Confusing relaxed and strict alignment. aspf=r (relaxed, the default) means a subdomain can authenticate for the parent. So bounces.yourapp.com aligns with yourapp.com. aspf=s (strict) requires exact match. Strict alignment is rarely what you want unless you control every sending subdomain.

Treating subdomains as covered. A DMARC record at _dmarc.yourapp.com covers yourapp.com and, by default via the sp= tag, all subdomains. But if you want different policies for mail.yourapp.com, you can publish a separate record at _dmarc.mail.yourapp.com. Without sp=, the parent policy is inherited.

A working setup, end to end

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

;; 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 is passing both SPF and DKIM with alignment, tighten to:

_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 typically takes a month if your sending is straightforward, three months if you have ten years of vendor sprawl.

How to verify it actually works

There are four commands every developer should know.

1. Check that the records exist:

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

2. Send a real message to a Gmail address and look at the headers. In Gmail, open the message and click "Show original." Look for:

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

The three things to check:

  • dkim=pass AND the domain after header.i=@ matches (or is a subdomain of) your From: domain.
  • spf=pass is nice, but for DMARC you need header.from=acme.app and the SPF/DKIM domain to align.
  • dmarc=pass is the bottom line. If this says dmarc=fail, dig into the SPF/DKIM lines to see which authenticated and which is failing alignment.

3. Send to check-auth@verifier.port25.com. Port25's verifier is the canonical "tell me if my email passes everything." It replies with a full report covering SPF, DKIM, DMARC, and SpamAssassin scoring. Free, no signup.

4. Set up MX-bound monitoring. Mailneo's free email header analyzer parses the raw headers and explains each line in plain English. For ongoing DMARC monitoring, Postmark's DMARC service is free and emails you a weekly digest.

The most common failure modes I see

After helping a few hundred apps debug deliverability, these are the three failures that account for maybe 80% of the tickets.

Failure 1: SPF passes, DKIM passes, DMARC fails. Almost always an alignment problem. The provider is signing with their domain, not yours. Check the d= tag of the DKIM-Signature header in the message; if it says d=ses.amazonaws.com instead of d=acme.app, you skipped the "verify your domain" step in the SES console and your provider is using its own DKIM key. Fix: complete domain verification in your provider's UI so DKIM signs as d=acme.app.

Failure 2: Mail from one specific feature lands in spam, everything else is fine. Almost always an unauthenticated shadow sender. A help-desk tool, a hiring system, a marketing intern's MailChimp account. Pull the headers from one of the spammed messages; the Authentication-Results line will name the actual sender and you can either add them to SPF, set up DKIM for them, or move them to a subdomain so they do not threaten your main domain's reputation.

Failure 3: Reports show DKIM passes for some messages, fails for others. Usually a content modification problem. A mailing list, a forwarding rule, or an antivirus appliance that rewrites the body before forwarding will invalidate the DKIM signature. There is nothing to fix in DNS; the answer is to make sure SPF aligns as a fallback (you cannot fix DKIM after the fact) and to use ARC if you are doing the forwarding yourself.

What about BIMI?

BIMI (Brand Indicators for Message Identification) is the fourth standard, and it is worth a brief note: BIMI displays your logo next to your messages in supported inboxes (Gmail, Apple Mail, Yahoo), but it requires p=quarantine or p=reject DMARC. If your DMARC is at p=none, BIMI does nothing. Fix DMARC first; BIMI is the reward for finishing.

Key takeaways

  • Modern inbox providers (Google, Yahoo, Microsoft) all require SPF, DKIM, and DMARC for any meaningful volume of mail; assume your app needs all three from day one.
  • DMARC requires alignment, not just passing SPF or DKIM; the domain authenticating has to match (or be a subdomain of) the domain in your From: header.
  • Always start at p=none and ramp through quarantine to reject. Two-thirds of DMARC records on the public internet are stuck at none and provide zero protection.
  • Verify with dig, with Gmail's Show Original, and with Port25's verifier. If three independent sources agree, your setup is correct.
  • The single biggest deliverability win for most apps is fixing alignment on transactional mail; everything else (warmup, content, send time) is downstream of authentication.

Frequently asked questions

Do I need SPF, DKIM, and DMARC for transactional email?

Yes. Receivers do not distinguish between marketing and transactional at the authentication layer. Password reset and receipt emails go through the same filters as newsletters, and unauthenticated transactional mail is just as likely to land in spam.

My provider says "we handle DKIM for you." Is that enough?

Only if their DKIM signing domain (d=) aligns with your From: domain. Many free tiers sign as the provider's own domain, which passes DKIM but fails DMARC alignment. Complete the "verify your domain" or "add custom domain" step in your provider's console.

Can I have more than one SPF record?

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

How long do DNS changes take to propagate?

Most modern providers (Cloudflare, Route 53, Google Cloud DNS) propagate in under a minute. Older providers can take up to your record's TTL (often 1 hour). Plan a one-hour window after publishing before testing.

Will adding these records break my existing email?

If you go straight to p=reject before testing, yes. Starting at p=none is delivery-neutral; it only collects reports. SPF and DKIM additions are safe as long as you do not remove existing senders from your SPF record by accident.

What happens to forwarded mail?

SPF fails on forwarded mail because the forwarding server is not in your SPF record. DKIM survives forwarding unless the forwarder modifies the body. DMARC passes if DKIM still aligns, which is why DKIM matters more than SPF for forwarded mail.

Do I need a separate DMARC record per subdomain?

Not unless you want different policies. By default, the parent domain's DMARC applies to subdomains via the sp= tag. Publishing a subdomain-specific record overrides the parent for that subdomain only.

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