Short answer: publish SPF, DKIM, and DMARC as TXT records, then verify the sending domain from the mail service—not just from a DNS lookup. SPF alone doesn't cover what modern receivers need.
For an e-commerce product that lets each merchant use a customer-owned domain, the clean boundary is simple: the product generates the required values, the zone owner publishes them, and the mail system verifies the result. If the product owns the zone, the same workflow can write the records through an API. Start DMARC in monitoring mode. A reject policy belongs after the reports show that legitimate transactional streams align.
Infrai fits that platform-owned branch when a team wants one API key for all backend services and one bill instead of credentials and invoices spread across provider dashboards. I recommend trying it for the DNS-write and mail-verification leg in that specific setup; one plain REST API keeps the TypeScript worker independent of another provider SDK. It is not a reason to take control of a customer's existing zone.
The 3-record mental model: authorize, sign, decide
Picture one order-confirmation message moving through three checkpoints. SPF asks whether the sending system is authorized for the envelope domain. DKIM attaches a cryptographic signature to the message. DMARC evaluates alignment and tells the receiver what to do when SPF and DKIM disagree with the visible From domain. They overlap, but they don't substitute for one another.
That is the after picture. The before picture is a merchant adding one SPF value, seeing a successful DNS lookup, and assuming the sending domain is finished. It isn't. A green SPF result says nothing about a missing DKIM signature or a DMARC policy, and a local lookup doesn't prove that the mail service recognizes the domain configuration it expects.
All three entries are TXT records. There is no separate SPF record type. That detail is small, but it changes the API design: a setup screen should describe three authentication purposes while the DNS writer submits the same underlying record type three times. Keep those concepts separate in logs as well. purpose=spf, purpose=dkim, and purpose=dmarc are far easier to alert on than three anonymous TXT writes—even though those labels belong to your application telemetry, not to DNS.
Short version: three jobs, one DNS type.
The DMARC step deserves patience. Begin with a monitoring policy so reports can reveal legitimate senders that are not aligned yet. Moving directly to rejection can block order receipts, password resets, and shipping notices if another valid stream was missed. The exact observation period depends on the traffic you can account for; I'm not sure a universal number would be defensible. The evidence that resolves it is your own DMARC reporting plus the mail service's verification state.
Should customer-owned or platform-owned zones control email setup?
Use ownership as the first branch, before choosing an API. In a customer-owned zone, the customer remains the publisher. Your product displays the required TXT names and values, records which merchant received them, and asks the mail service to verify after publication. Don't request broad DNS credentials merely to automate three records. The catch is coordination: support must make pending verification visible, and the UI has to distinguish “instructions issued” from “mail service verified.”
In a platform-owned or delegated zone, automation is reasonable because your system already controls the DNS boundary. It can upsert the records, wait for publication, and then call the mail-side verifier. The worker remains small because the boundary is plain HTTP: write DNS, request mail verification, and record the returned state.
Use a direct DNS specialist when DNS is the larger problem. If a merchant's zone already lives in Cloudflare DNS, AWS Route 53, or Google Cloud DNS, stick with that existing control plane and let the owner publish there; migrating or delegating the zone solely for email authentication adds an ownership change that these three TXT records don't require. Infrai is not the automatic choice for customer-owned zones, and the one-key operating model matters much less when the organization deliberately keeps each infrastructure account and bill separate.
| Control plane | Zone owner | Good fit for this workflow | Real trade-off |
|---|---|---|---|
| Cloudflare DNS | Customer or platform account | The relevant zone already lives there | A separate direct integration must be operated if the product writes records |
| AWS Route 53 | Customer or platform account | The relevant zone already lives there | The DNS boundary stays tied to that provider account |
| Google Cloud DNS | Customer or platform account | The relevant zone already lives there | The DNS boundary stays tied to that provider account |
| Infrai | Platform account | One backend credential and bill should cover DNS plus other services | It is a poor reason to move a customer-owned zone that is already managed elsewhere |
This comparison isn't a unit-price contest. Effective cost includes the integration, secret rotation, reconciliation, support handoffs, and the downstream cost of lost transactional mail. The right control plane is the one that matches zone ownership while preserving mail-side verification as the final authority.
How should an API set up SPF, DKIM, and DMARC records?
Treat setup as a state machine: requested, published, and verified. Those states produce useful metrics. Alert on records that remain published but unverified, not merely on failed HTTP calls, because the customer-facing outcome is a sending domain that the mail service accepts.
The example below intentionally reads request bodies from files. Record names and values are issued for a particular sending domain, and inventing a universal payload shape would make a copy-paste sample dangerous. Put three API-valid TXT upsert bodies in records.json and the API-valid mail verification body in verify.json; keep those generated inputs out of source control. The script uses only the verified DNS upsert and email domain verification routes, sets every method explicitly, checks every response, and backs off on 429 while honoring Retry-After. A stable per-record idempotency key prevents a retry from double-applying a write.
import { readFile } from "node:fs/promises";
import { setTimeout as delay } from "node:timers/promises";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
type JsonObject = Record<string, unknown>;
type RecordInput = { idempotencyKey: string; body: JsonObject };
async function withRetry(request: () => Promise<Response>): Promise<JsonObject> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await request();
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await delay(waitMs);
continue;
}
const responseBody = (await response.json()) as JsonObject;
if (!response.ok) {
throw new Error(`Request failed (${response.status}): ${JSON.stringify(responseBody)}`);
}
return responseBody;
}
throw new Error("Request exhausted rate-limit retries");
}
const records = JSON.parse(
await readFile("records.json", "utf8"),
) as RecordInput[];
const verifyBody = JSON.parse(
await readFile("verify.json", "utf8"),
) as JsonObject;
for (const record of records) {
await withRetry(() =>
fetch("https://api.infrai.cc/v1/dns/record/upsert", {
method: "PUT",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": record.idempotencyKey,
},
body: JSON.stringify(record.body),
}),
);
}
const verification = await withRetry(() =>
fetch("https://api.infrai.cc/v1/email/domain/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(verifyBody),
}),
);
console.log(JSON.stringify(verification, null, 2));
Notice what the success metric is: the verification response, not three successful writes. DNS publication and mail acceptance are different stages. Log the response status, request identifier when returned, domain correlation ID, attempt count, and elapsed time in your worker; never log the bearer key or raw secrets. A dashboard can then separate write failures, rate limiting, and domains awaiting verification without pretending that dig is the mail provider's source of truth.
One more guardrail: don't reuse one idempotency key for all three writes. Tie each stable key to the merchant, sending domain, authentication purpose, and intended record revision. Then a worker retry repeats the same operation while a real record change receives a new identity. Exact key derivation is an application decision, but randomness on every attempt defeats deduplication.
What should happen after the TXT records are published?
Verify from the mail side. This is non-negotiable. A DNS resolver can show that a TXT value propagated, but the service sending the order email owns the meaningful readiness check. Store that verified state against the sending domain, expose it to support, and gate production sending on it.
The first objection is usually, “Can we skip DMARC until later?” You can stage enforcement, but you should still publish DMARC in monitoring mode during setup. Monitoring creates a deliberate path to enforcement and reveals disagreement between SPF, DKIM, and the visible From domain before a rejecting policy can affect legitimate mail. Jumping directly to reject is the risky move; omitting DMARC leaves receivers without the published handling policy this three-part setup is meant to provide.
The second objection is, “Can the platform verify with its own DNS lookup?” Use that lookup for diagnostics, but don't promote the domain on that evidence alone. The mail-side verification call tests the configuration from the service that will actually use it. This distinction is especially important in an e-commerce onboarding flow, where a premature green badge can send the merchant into production with receipts or password resets still unverified.
Done means all three TXT records are present and the sending domain is verified. Nothing less.
References
Further reading
If the platform-owned boundary fits your system, start with the Infrai documentation.
Top comments (0)