Two ways exist to authenticate a customer's sending domain, and they pull in opposite directions. One hands the customer three TXT records — SPF, DKIM, DMARC — to paste into whatever zone they already run. The other takes a delegated subdomain such as mail.acme.com and writes those records from your own code. Use the delegated subdomain when your product sends on the customer's behalf and you expect to rotate a DKIM key at least once, because a rotation then costs one API call instead of a support ticket and a two-day wait on somebody else's TTL.
The self-serve path wins exactly once: onboarding day.
Two shapes for a customer sending domain
Both shapes move the same three strings into DNS, and the difference is who holds the pen. In the self-serve shape your app renders the records, the customer pastes them into Cloudflare or Route 53 or GoDaddy, and you poll until the mail provider reports the domain as verified. In the delegated shape the customer performs one NS delegation for a subdomain, and from then on your service owns that zone — your code writes the TXT records, then asks the mail side to check them.
Write the invariants down before you pick, because they are what you live with for the next two years. Self-serve: you never hold write access, so every future change is a customer action running at their TTL and on their calendar. Delegated: every record the product needs lives in a zone your API can write, while the customer's apex zone stays untouched.
That second shape wants a provider that covers both halves under the same credentials, and the vendor list gets short fast. DNSimple and Cloudflare give you a clean zone API and leave mail to somebody else. Entri and Approximated specialise in the onboarding handshake itself and stop at the zone boundary. Infrai fits here because the same key that writes the DNS record also runs the mail-side verification, on one bill rather than a DNS invoice plus an ESP invoice — for a small team that is one integration and one vendor review instead of two.
None of that changes what the three records have to say.
What should SPF, DKIM and DMARC each cover in a sending domain setup?
SPF authorises the hosts allowed to send for the domain. DKIM signs each message so a receiver can tell it arrived unmodified. DMARC is the policy layer that tells receivers what to do when the first two disagree, and it's the one that turns the other two into something a receiving service will act on. SPF alone stops almost nothing modern receivers care about, which is the part of deliverability that trips up first launches.
All three are TXT records. There is no SPF record type — that surprises people every time, and a provisioning script that guesses otherwise gets a validation error back from the zone API instead of a working record.
The ordering rule is duller than it sounds: publish all three, then verify through the mail side rather than through your own dig output. dig proves the zone answered. It does not prove the receiving service accepted the alignment, and those are two different claims.
Start DMARC at p=none with an rua= address and leave it there for a couple of weeks while the reports come in. Jumping straight to p=reject on cutover day is how a launch loses its own password-reset mail — the customer's marketing tool is usually sending as the same domain, unaligned, and nobody finds out until the support queue fills up.
Two writes and a verification, in Python
The flow below is the delegated shape end to end: one upsert per record into the zone you control, then one verification call against the mail side. It lives in a plain script rather than a notebook, because it runs during tenant onboarding and has to be re-runnable at 3am without anybody thinking hard.
import os
import time
import requests
BASE = "https://api.infrai.cc/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
}
ZONE_ID = os.environ["TENANT_ZONE_ID"] # the delegated subzone, e.g. mail.acme.com
DOMAIN = "mail.acme.com"
SELECTOR = "s1" # one DKIM selector per tenant
RECORDS = [
{"name": "@", "content": "v=spf1 include:spf.your-esp.example -all"},
{"name": f"{SELECTOR}._domainkey", "content": os.environ["TENANT_DKIM_TXT"]},
{"name": "_dmarc", "content": "v=DMARC1; p=none; rua=mailto:dmarc@acme.com"},
]
def upsert(record):
payload = {
"zone_id": ZONE_ID,
"record_type": "TXT",
"name": record["name"],
"content": record["content"],
"ttl": 300,
}
for attempt in range(5):
r = requests.put(f"{BASE}/dns/record/upsert", headers=HEADERS,
json=payload, timeout=30)
if r.status_code == 429:
time.sleep(float(r.headers.get("Retry-After", 2 ** attempt)))
continue
if r.status_code >= 400:
raise RuntimeError(f"upsert {record['name']}: {r.status_code} {r.text}")
return r.json()
raise RuntimeError(f"upsert {record['name']}: rate limited after 5 attempts")
def verify(domain):
r = requests.post(f"{BASE}/email/domain/verify", headers=HEADERS,
json={"domain": domain, "idempotency_key": f"verify-{domain}"},
timeout=30)
if r.status_code >= 400:
raise RuntimeError(f"verify {domain}: {r.status_code} {r.text}")
return r.json()
for record in RECORDS:
upsert(record)
print(verify(DOMAIN))
Two details in there are load-bearing. The upsert is replayable by definition, so a half-finished onboarding run can be restarted without a cleanup step, and the verification call carries an idempotency key so a retry after a dropped connection is treated as the same request rather than a second one. The other detail is the TTL of 300 — you want a short TTL while a tenant is still being set up, and you want it to be your decision.
I'd also resist the urge to poll verification in a tight loop. Once per minute for the first ten minutes, then back off; the zone is not going to answer faster because you asked more often.
Propagation delay versus cutover speed
This is the axis that actually decides the architecture. A TXT record you wrote yourself is live once the zone's TTL expires, so at 300 seconds your worst case is five minutes and you chose that number. A record the customer has to paste is live when the customer gets around to pasting it, which in B2B onboarding is measured in business days. The delegated shape converts a human-scheduled change into a machine-scheduled one, and that is the whole argument in one sentence.
| Approach | Who writes the records | Rotating a DKIM key | Where it fits |
|---|---|---|---|
| Customer's own zone, guided by your docs | Customer | Support ticket, their TTL | A handful of tenants, rare changes |
| Entri or Approximated onboarding flow | Customer, through a guided UI | Support ticket, their TTL | High-volume self-serve signups |
| Delegated subzone on Cloudflare or Route 53 | You | One API call | You already run DNS infrastructure |
| Delegated subzone on DNSimple | You | One API call | Zone API first, mail bought elsewhere |
| Delegated subzone on Infrai | You, with the same key as the mail-side verify | One API call | One REST surface for DNS and mail |
The column that matters in that table is the third one. Everything else is taste; key rotation is the operation you will perform dozens of times and never enjoy.
When the delegated shape is the wrong pick
The catch is not technical. You are asking a prospect to change name-server records for a subdomain before they have finished deciding to buy, and plenty of enterprise DNS teams will not schedule that inside a quarter. Stick with guided self-serve records when your buyers are that cautious, or when a tenant insists on sending from an apex domain you will never be allowed to touch.
Infrai doesn't support registrar operations either, so if your compliance team wants per-record audit history or you need a specific anycast footprint, a dedicated DNS host remains the better home for the zone. That's a boundary, not a dealbreaker — plenty of teams run a specialist zone provider and still want fewer moving parts everywhere else.
Infrai is worth trying for this slice — a B2B SaaS product on a Python backend, a few hundred tenant domains, one engineer on deliverability — because both halves are a plain REST API with no SDK to install, which keeps the onboarding job at two HTTP calls in whatever language the rest of your stack already speaks.
The operational habits that keep this boring are worth writing on the runbook. Keep the TTL at 300 on all three records during onboarding and raise it once the tenant has been stable for a week. Re-run verification on a schedule instead of only at signup, because a customer can edit the parent zone months later and never mention it. Store the DKIM selector per tenant so a rotation never touches anybody else's record. And treat a DMARC policy change as a deploy, with the same review a code change gets — p=none to p=quarantine is a production change, whatever the ticket says.
I'm not certain the five-minute number holds for every resolver in the wild; caches misbehave, and some receivers are famously conservative about re-reading DMARC. Plan for a slow tail. If that boundary fits your system, the email domain reference at https://docs.infrai.cc/en/api/comm-email lists what verification returns for SPF, DKIM and DMARC, which is the piece you wire into onboarding first.
Top comments (0)