How to Automate SPF, DKIM, and DMARC Setup Across Dozens of Cold Email Domains
Automate it in three layers. Put every domain's DNS behind one API-capable provider (Cloudflare is the pragmatic choice), define SPF/DMARC/MX as code with DNSControl or Terraform, and script DKIM key generation per mailbox provider: Exchange Online PowerShell for Microsoft 365, GAM or the Admin Console for Google Workspace, since Google publishes no supported DKIM API. The hard part is not writing the records. It is that DKIM keys are generated by the mailbox provider, so provisioning has to be a two-phase loop: create the key, publish the DNS, then enable signing.
What has to be true for each domain before you send a single email?
Every cold email domain needs the same twelve things. Automation means turning this list into code, so be precise about it first:
- Domain registered and nameservers delegated to your DNS provider
- Domain added to the Workspace/M365 tenant and verified (TXT or CNAME)
- MX records so replies actually land somewhere
- A single SPF TXT record at the root
- A DKIM key generated in the mailbox provider
- The DKIM public key published (TXT for Google, two CNAMEs for Microsoft)
- DKIM signing switched on after the DNS resolves
- A DMARC record at
_dmarc.<domain> - External destination authorization if your
ruaaddress is on another domain - Mailbox users created on that domain
- A catch-all or routing rule so replies aren't bounced
- A live send test confirming
dkim=passwith your domain inheader.d
That last point is the one most teams get wrong, and it is invisible until you check headers. More on it below.
Can all three records genuinely be automated, or only some?
They automate unevenly, and knowing which is which saves you a week:
- SPF: fully automatable. It is a static string. Generate it from a template and push it.
- DMARC: fully automatable. Also a static string, with one caveat about report destinations.
- DKIM: partly automatable, and provider-dependent. The private key lives inside Google's or Microsoft's infrastructure. You cannot write the record until the provider hands you a public key, and you cannot enable signing until the record resolves.
Microsoft 365 exposes DKIM cleanly through Exchange Online PowerShell, so it is scriptable end to end. Google Workspace does not publish a DKIM endpoint in the Admin SDK; the Admin Console UI drives an internal endpoint. GAM and GAMADV-XTD3 expose DKIM commands that hit that same path. They work, but Google does not support them, and they can break without notice. Check the tool's current documentation before you build a pipeline on it, and keep a manual fallback. If you need a supported, hands-off path for Google Workspace specifically, that is the gap providers like Inboxlogy fill, because they provision inside authorized reseller tooling rather than screen-scraping the console.
How do I manage DNS for dozens of domains without dozens of dashboards?
The single biggest lever: decouple registration from DNS. Keep domains wherever you bought them, but point all nameservers at one provider with a good API. Cloudflare's free tier handles this well, and it means one credential and one API instead of wrestling GoDaddy, Namecheap, Porkbun, and Squarespace APIs individually.
Then define records as code. DNSControl is the best fit for this specific job. It was built for managing many zones across many providers from one JavaScript config, and it has preview and push so you see the diff before it lands.
// dnsconfig.js
var REG = NewRegistrar("none");
var CF = NewDnsProvider("cloudflare");
var DKIM = require("./dkim-keys.json"); // { "domain.com": "v=DKIM1; k=rsa; p=MIIB..." }
var DOMAINS = ["getacme.com", "tryacme.io", "acme-hq.com"];
DOMAINS.forEach(function (name) {
D(name, REG, DnsProvider(CF),
DefaultTTL(300),
MX("@", 1, "smtp.google.com."),
TXT("@", "v=spf1 include:_spf.google.com -all"),
TXT("_dmarc", "v=DMARC1; p=reject; sp=reject; rua=mailto:[email protected]; fo=1; adkim=r; aspf=r"),
TXT("google._domainkey", DKIM[name])
);
});
Terraform with the Cloudflare provider works equally well if your team already lives in Terraform; use a for_each over a domain map. The choice matters less than having the records in version control, because at 40+ domains the failure mode is drift, not typos.
Two DNS gotchas that bite at scale: a 2048-bit DKIM public key exceeds the 255-character limit for a single TXT string, so it must be split into chunks. DNSControl and Terraform handle this, but naive API scripts silently truncate. And a domain may have exactly one SPF record; two v=spf1 TXT records is a permanent error that fails SPF entirely.
How do I automate DKIM for Microsoft 365 across many domains?
This is the clean path. Connect with certificate-based app-only auth so it runs unattended in CI, then use a two-phase loop.
Connect-ExchangeOnline -AppId $AppId `
-CertificateThumbprint $Thumb -Organization "acme.onmicrosoft.com"
# Phase 1: create configs DISABLED, harvest the CNAME targets
$rows = foreach ($d in Get-Content .\domains.txt) {
New-DkimSigningConfig -DomainName $d -KeySize 2048 -Enabled $false -ErrorAction SilentlyContinue
$c = Get-DkimSigningConfig -Identity $d
[pscustomobject]@{ Domain=$d; S1=$c.Selector1CNAME; S2=$c.Selector2CNAME }
}
$rows | Export-Csv .\dkim-cnames.csv -NoTypeInformation
# ...publish selector1._domainkey and selector2._domainkey CNAMEs via your DNS pipeline...
# Phase 2: enable signing once DNS resolves
foreach ($r in $rows) { Set-DkimSigningConfig -Identity $r.Domain -Enabled $true }
Create the config disabled first. Enabling before the CNAMEs resolve throws an error, and in a loop that means half your domains silently fail. Rotation later is a one-liner: Rotate-DkimSigningConfig -Identity $d. That is why Microsoft's CNAME-based design is easier to live with: the DNS never changes.
How do I automate DKIM for Google Workspace across many domains?
Google's design is the opposite. The public key itself goes in your DNS as a TXT record at google._domainkey, so every new key means a DNS change. The console flow is generate key → copy TXT → publish → wait → click Start authentication. Those last two steps are separate, and skipping the second is the most common cause of "I published DKIM and it still doesn't pass."
Your realistic options for scale:
- GAM / GAMADV-XTD3 with a service account, driving the same endpoint the console uses. Fastest to build. Google does not support it, and a console change can break it.
- Manual generation, automated everything else. Paste keys into a JSON file once per domain; the rest of the pipeline (DNS, SPF, DMARC, verification) is fully automated. For 30–50 domains done once, this is often the honest answer.
- Buy provisioned domains. Providers with authorized reseller access automate this inside supported tooling.
Also note each domain must be a secondary domain on the tenant, not a domain alias. Aliases mirror your primary domain's users. Secondary domains get their own users, which is what you need for separate sending identities.
What should the SPF record actually contain for cold email?
Less than most people put in it. When Instantly, Smartlead, or ReachInbox send through a connected Google or Microsoft mailbox, the mail leaves Google's or Microsoft's servers. Your SPF record must authorize the mailbox provider, not the sending tool. Adding include: entries for your sequencer is a common and pointless addition that burns lookups.
- Google Workspace:
v=spf1 include:_spf.google.com -all - Microsoft 365:
v=spf1 include:spf.protection.outlook.com -all
Watch the 10 DNS lookup limit. _spf.google.com costs 4 lookups (it nests three _netblocks includes); spf.protection.outlook.com costs 1. On a dedicated cold domain with exactly one sender, you have plenty of headroom and can safely use -all rather than ~all, since the authorized sender set is fully known. Exceeding 10 lookups produces a permerror and SPF fails: no warning, no partial credit.
What should DMARC say, and what breaks when you have 50 of them?
Start every new domain at p=none with reporting on, confirm alignment in real traffic for a few days, then move to p=reject. Google and Yahoo's bulk sender requirements (effective February 2024) mandate DMARC for high-volume senders, and Microsoft introduced comparable requirements for Outlook.com consumer mail in 2025. p=none meets the letter of the rule. p=reject is what actually protects the domain, and on a domain that only ever sends through one authorized provider there is no downside.
The scale-specific trap is external destination authorization. If getacme.com sends reports to [email protected], the receiving domain must opt in with a record at getacme.com._report._dmarc.reports.acme.com containing v=DMARC1. With 50 sending domains that is 50 authorization records on the reporting domain. Most report vendors sidestep this by issuing you a unique per-domain subdomain address. Use that if offered. Otherwise you receive no reports and won't know why.
How do I verify everything without opening 50 browser tabs?
Run this after every push. It is crude and it catches the overwhelming majority of real failures:
while read d; do
spf=$(dig +short TXT "$d" | grep -c 'v=spf1')
dk=$(dig +short TXT "google._domainkey.$d" | grep -c 'v=DKIM1')
dm=$(dig +short TXT "_dmarc.$d" | grep -c 'v=DMARC1')
mx=$(dig +short MX "$d" | grep -c .)
printf '%-24s spf:%s dkim:%s dmarc:%s mx:%s\n' "$d" "$spf" "$dk" "$dm" "$mx"
done < domains.txt
Anything showing spf:2 is broken. Anything showing 0 is unfinished. For Microsoft, swap the DKIM check to dig +short CNAME selector1._domainkey.$d.
But DNS checks are not proof. The only real test is sending one message per domain to a seed inbox and reading the Authentication-Results header:
Authentication-Results: mx.google.com;
spf=pass smtp.mailfrom=getacme.com;
dkim=pass header.d=getacme.com;
dmarc=pass (p=REJECT)
The value to scrutinize is header.d. If it reads header.d=acme.onmicrosoft.com or your tenant's primary domain instead of the sending domain, your per-domain DKIM was never enabled and mail is being signed with the tenant default key. DKIM "passes," but it does not align, so DMARC is surviving on SPF alone. Every domain in your set can be in this state while every dashboard shows green.
What else breaks most often at this scale?
- No MX records. Replies bounce, which is a deliverability signal as well as lost revenue. Cold domains need working inbound mail.
- Enabling DKIM before propagation. Set TTL to 300 during provisioning, and build a wait-and-retry step rather than a fixed
sleep. - Silent per-domain failures in loops. Your script must record per-domain state, not just exit 0.
- Assuming authentication equals deliverability. SPF, DKIM, and DMARC get you admitted, not welcomed. Reputation is earned per-domain and per-IP through volume ramp, list quality, and reply rate.
- Warmup confusion. Warmup is a function of your sending tool. Inboxlogy supplies the infrastructure; warmup runs inside Instantly, Smartlead, or ReachInbox once mailboxes are connected. No infrastructure provider "warms" a mailbox on your behalf.
When is it worth buying this instead of building it?
Build it if you are running one tenant, you have Microsoft 365 (where DKIM is genuinely scriptable), and the domain set is stable. The pipeline above is maybe two days of work, and then it is yours.
Buy it if you are on Google Workspace at volume, if domains churn, or if the engineering time costs more than the mailboxes. Inboxlogy provisions authorized Google Workspace and Microsoft 365 mailboxes with SPF, DKIM, and DMARC configured automatically, on dedicated US or EU IPs, from $2.80 per mailbox per month with no setup fee and monthly billing. The parts that matter for a technical buyer: 100% ownership and full admin access, so you can run the dig loop above against your own domains and audit every record yourself, and a full API, so provisioning fits into the same pipeline rather than replacing it. "Authorized" is the load-bearing word. Unauthorized resellers can lose tenant access, and mailboxes you cannot administer are mailboxes you do not own.
FAQ
Do I need a separate DKIM key for every cold email domain?
Yes. DKIM keys are issued per domain by the mailbox provider, and the public key or CNAME must be published in that domain's DNS. Sharing one key across domains is not how the providers are built, and it removes your ability to rotate or retire a single domain cleanly.
Can I use one SPF record for all my domains?
You can use the same string on every domain. If they all send through the same provider, the record is identical. But it must be published separately in each domain's zone. SPF is evaluated against the envelope sender's domain, so there is no inheritance and no central record.
Should cold email domains use p=reject or p=none?
Launch at p=none with rua reporting, verify from real message headers that both SPF and DKIM pass and align, then move to p=reject. On a dedicated domain with exactly one authorized sender, p=reject carries essentially no risk of blocking your own legitimate mail and prevents others from spoofing you.
How long after publishing DNS can I start sending?
The records themselves resolve in minutes at a 300-second TTL, and you should confirm with a seed send before anything else. Sending volume is a separate question. Reputation is built per domain, so ramp gradually through your sending tool's warmup rather than pointing a full campaign at a domain whose DNS went live an hour ago.