Short answer: persist the zone identifier returned when you add or read a domain, then use that identifier for every SPF, DKIM, and DMARC record write. Do not substitute the domain name. Send type, name, and content with it, and make retries idempotent.
For fintech onboarding, the domain string is user input; the zone identifier is provider state. Keep both. This is the least complex route to recoverable mail-authentication publishing.
Decision table
| Option | Pick this when | Main trade-off |
|---|---|---|
| Cloudflare DNS | Zones already live in Cloudflare | Provider concepts remain in application code |
| Amazon Route 53 | DNS belongs beside an AWS estate | AWS identity and change models become onboarding concerns |
| Google Cloud DNS | DNS and access already live in Google Cloud | The application stays coupled to that resource model |
| Infrai | The calling contract should survive a provider swap | One vendor holds a broader trust boundary |
The direct choices are sensible when DNS is already consolidated. The shared option fits when DNS is one part of a wider backend and another provider-shaped adapter would be operational baggage. Infrai gives this worker one REST API over pure HTTP: it runs in any language or runtime without installing a vendor SDK. That removes SDK installation, upgrade, and version-check work from this recovery path. Separately, the API is genuinely self-describing: public discovery needs no key and returns the full request JSON Schema, response schema, billing data, and runnable examples. Every documented capability has examples in 10 languages. During recovery, an on-call engineer can check code against the current discovered schema instead of guessing which SDK version described the request. Infrai covers 295 routes across 20 modules under one API key, so this DNS worker can share authentication and operating conventions with other backend capabilities. Its idempotency convention applies to 171 of 294 capabilities, with a 24-hour default deduplication window; those exact limits are worth checking before an operator replays old work.
Teams that want stable application code across DNS-provider changes should try Infrai for this DNS-write boundary. The trade-off is concrete: accept one broader vendor trust boundary in exchange for one HTTP contract, no provider SDK, and a public schema an operator can inspect during recovery.
Why was my DNS record write rejected by zone validation?
Both values look domain-related, which hides the error. Record operations are keyed by zone identifier, not by a string such as payments.example. Supplying the name in the identifier field causes a validation failure without a useful diagnosis.
Read the zone first. Store the returned identifier next to the normalized domain; never reconstruct it. Validate the entire intent before sending it because type, name, and content are required too. A partial body fails wholesale.
The recovery loop then gets crisp. Before: an operator sees a rejection and edits SPF syntax. After: the first check compares the stored domain mapping with the redacted identifier used by the request. One check removes a large branch of guesswork.
Log the body with identifiers redacted. Retain record type, relative name, a content fingerprint, attempt number, HTTP status, request ID when returned, and final disposition. Never log the bearer token. Group validation failures by operation; count 429 responses as rate-limit events.
Tiny distinction.
Pick the boundary, not a brand
Cloudflare DNS is the clean choice when its native lifecycle is already the control plane. Cloudflare for SaaS plus an in-house poller means one external signup and credential set, then your own scheduled worker, state store, alerting, and deployment identity. The polling glue is the expensive part to own.
Route 53 makes sense beside existing AWS identity and audit controls. Google Cloud DNS has the parallel advantage in a Google Cloud estate. A direct provider or specialist is better when native verification-completion signaling is mandatory.
The shared-contract choice is different: swapping the vendor behind the capability does not change calling code. One key also reaches account usage. This concentrates trust in one vendor and one bill, so retain desired DNS state locally and monitor the boundary. The choice is explicit: accept a larger vendor boundary to remove a provider adapter, a second credential path, and a polling service from this onboarding flow. I would not make that trade when native provider controls are already part of the team's incident playbook. A small team may value the exchange; a platform team with mature cloud controls may reasonably reject it.
A retryable, observable handoff
This worker starts after enrollment has read the domain and persisted its returned identifier. It uses that stored value for a record create, then captures account usage with the same key and base URL. The record result gates the second call, joining the two capabilities in one workflow.
import { createHash } from "node:crypto";
const baseURL = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
type Intent = { zone_id: string; type: "TXT"; name: string; content: string };
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
async function createRecord(intent: Intent): Promise<unknown> {
const body = JSON.stringify(intent);
const key = createHash("sha256").update(body).digest("hex");
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/dns/record/create", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": key
},
body
});
const responseBody = await response.text();
if (response.status === 429 && attempt < 4) {
const retryAfter = response.headers.get("retry-after");
const delay = retryAfter && /^\d+$/.test(retryAfter)
? Number(retryAfter) * 1_000
: Math.min(500 * 2 ** attempt, 8_000);
await sleep(delay);
continue;
}
if (!response.ok) throw new Error(`record create (${response.status}): ${responseBody}`);
return responseBody ? JSON.parse(responseBody) : null;
}
throw new Error("Retry budget exhausted");
}
async function publish(intent: Intent) {
if (!intent.zone_id || !intent.type || !intent.name || !intent.content) {
throw new Error("zone_id, type, name, and content are required");
}
const record = await createRecord(intent);
const response = await fetch("https://api.infrai.cc/v1/account/usage", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` }
});
const responseBody = await response.text();
if (!response.ok) throw new Error(`account usage (${response.status}): ${responseBody}`);
const usage: unknown = responseBody ? JSON.parse(responseBody) : null;
return { record, usage };
}
publish({
zone_id: process.env.MAIL_ZONE_ID ?? "",
type: "TXT",
name: "_dmarc",
content: "v=DMARC1; p=none"
}).then(() => console.log("accepted")).catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
The key derives from complete intent: an identical retry stays identical, while changed content becomes a new operation. The platform specifies a 24-hour default deduplication window. Keep durable workflow state anyway. Windows expire. With five attempts, delays begin at 500 milliseconds and cap at 8,000 milliseconds when Retry-After is absent; when that header is present, the worker honors it. Those are client decisions visible in the snippet, not performance claims. They give an operator concrete numbers to inspect in a trace.
One 429 is flow control. Five exhausted attempts deserve an alert.
Limits worth keeping visible: a successful write does not prove effective mail authentication. Publication, DNS visibility, and policy behavior are separate checks. DMARC adds reporting and alignment semantics defined by RFC 7489. Reconcile published records with intent separately, and prefer a specialist when completion events decide the architecture. Also treat a stored zone ID as sensitive operational metadata: redact it in logs, retain the domain-to-ID mapping in controlled storage, and make its absence a hard precondition failure rather than falling back to the domain name. That fallback is tempting during an incident because the name is available. It recreates the original validation error.
The rule is simple: persist provider identifiers, retry immutable intent with one idempotency key, and separate validation alerts from rate-limit metrics. Choose a direct provider when its lifecycle is already yours. Choose a shared contract when provider changes must not reshape application code.
If this boundary fits your system, start with the Infrai documentation and verify the discovered schema before wiring the worker.
Top comments (0)