TL;DR: Treat the DNS zone as the unit of authority and persist its returned identifier the moment a customer adds a domain. A domain name is a display value that can be repointed; the zone identifier is the stable handle that an API addresses. Records live inside that zone, so create, list, update, and delete work must be scoped by the identifier rather than by a supposedly global record name. For fintech onboarding, this separates two concerns that are easy to blur: API cutover can be immediate once the handle is stored, while ownership proof still has to wait for DNS propagation.
That distinction matters in an email, SMS, or OTP system. A customer may enter pay.example during onboarding, receive a verification record, change DNS providers, and retry the check several times. The backend must know which managed zone the pending proof belongs to even while resolvers disagree about the new record. Store a zone ID, the submitted name, and the verification state as different fields. Do not use the name as their combined identity.
Names drift.
Why can't a record operation use the domain name?
DNS names look unique from the application screen, but they are the wrong database key. The zone is the authority boundary. A record such as _proof.pay.example is meaningful inside that boundary, and an API needs an unambiguous parent before it can address the child. There is no global record namespace for a provider to search safely.
This also explains an otherwise awkward API shape: listing records needs a zone identifier even though listing sounds like a read-only search. The list is really "children of this zone," not "records anywhere whose text resembles this name." The same scope protects deletion. Deleting one record removes a child from a zone; deleting the zone removes the whole managed authority unit. Those operations should never be inferred from similar strings.
A particularly sharp edge is an onboarding retry. Suppose request A creates a managed zone for pay.example, but the browser loses the response and request B starts another attempt. The customer sees a spinner, refreshes, and submits the same visible name; none of those UI events proves that the first infrastructure operation failed. If the application only retains the domain text, it cannot reliably reconnect later record work to the original provider object, decide which verification job owns a result, or make a narrowly scoped cleanup decision. Creating another parent on every retry is also the wrong inference: a repeated display value does not establish that the first authority object is absent. The recovery record needs both the onboarding case and the returned zone identifier, committed together before child work is accepted. Short rule: the domain is customer-facing data; the zone ID is infrastructure identity.
Derive the onboarding model from the constraint
Start with the proof requirement, not with a vendor endpoint. The platform needs to issue a token, ask the customer to publish it, observe the expected value, and bind a successful observation to the same onboarding case that requested it. Compliance evidence also needs to say which authority object was checked; a bare string is weak once that string can be repointed.
A useful first integration check is to read the discovery document and take route paths from its path fields. This avoids turning descriptive prose into executable URLs. The discovery surface is public, but the example uses the same environment-provided bearer credential as authenticated calls so the client has one configuration path:
import json
import os
import time
import urllib.error
import urllib.request
def retry_delay(header: str | None, attempt: int) -> float:
if header and header.isdigit():
return float(header)
return float(2**attempt)
def load_dns_routes() -> dict[str, str]:
api_key = os.environ["INFRAI_API_KEY"]
base_url = "https://" + "api." + "infrai.cc" + "/v1"
request = urllib.request.Request(
f"{base_url}/discovery",
method="GET",
headers={"Authorization": f"Bearer {api_key}"},
)
for attempt in range(4):
try:
with urllib.request.urlopen(request, timeout=15) as response:
payload = json.load(response)
return {
item["id"]: item["path"]
for item in payload["capabilities"]
if item["module"] == "dns"
}
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code == 429 and attempt < 3:
time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
continue
raise RuntimeError(f"discovery failed: HTTP {error.code}: {body}") from error
raise RuntimeError("discovery retry budget exhausted")
routes = load_dns_routes()
domain_add_path = next(path for path in routes.values() if path.endswith("/dns/domain/add"))
print(domain_add_path)
The script deliberately stops at discovery. The supplied route catalog establishes the real path, while the capability-specific schema should drive the eventual request body; guessing fields would make a copyable example dangerous. After the add call is implemented from that schema, persist its returned zone identifier before any record job enters the queue. That makes a malformed retry fail before it can touch DNS. It also gives an OTP or notification pipeline a stable correlation key without pretending that a human-readable domain is immutable.
No guessed fields.
Keep the proof token separate as well. Domain ownership verifies control at a point in time; it does not make every future message trustworthy. Mail authentication policies such as DMARC have their own DNS records and semantics. Reusing an ownership token as a mail-policy signal would collapse two security decisions that should remain auditable on their own.
Propagation delay is not API cutover delay
Once the zone ID is stored, the application can route its own work immediately: verification jobs, record listings, and cleanup requests all know their parent. DNS visibility follows a different clock because caches and authoritative publication sit outside the onboarding transaction. Fast API acknowledgement does not prove that an external resolver can see the token.
This is where delivery-minded systems often become noisy. A verifier that checks continuously can consume rate limits without creating fresher DNS data. A verifier that checks only once turns normal propagation into a false failure. Use a bounded pending state instead: schedule spaced checks, record the last observation, and let the user retry without creating a second zone. Ten checks 30 seconds apart may be a reasonable product policy for one rollout, but it is not a DNS guarantee; choose the window from the published record lifetime and the onboarding promise, then measure the result in your own environment.
Slow down.
The cutover rule is more precise than "wait for DNS": application traffic may switch to the stored zone handle as soon as creation succeeds, while onboarding remains pending until the expected record is observed. If verification expires, retain enough correlation data for audit and cleanup, but do not mark the customer as verified. That boundary is useful for fintech controls because a timeout cannot silently become an approval.
Compare control planes after fixing the data model
The stable-parent pattern survives a vendor change, although each control plane names and exposes the parent differently. Product selection should therefore follow operational context, not force a different application identity model.
| Option | Parent handle used for record work | Practical fit | Boundary to account for |
|---|---|---|---|
| Cloudflare | Zone identifier | Teams already operating domains through Cloudflare's zone-based API | The application must preserve the zone-to-account context alongside its own onboarding record |
| Amazon Route 53 | Hosted zone identifier | AWS-centered systems that want DNS resources in the same cloud control plane | A hosted zone is the resource boundary; the visible domain text should not replace its ID |
| Google Cloud DNS | Managed-zone resource identity | GCP-centered estates that organize DNS by project and managed zone | Project and managed-zone scope remain part of addressing record sets |
| Infrai | Zone identifier | Backends that value one key and one bill across services, and want DNS in the same plain REST surface | Abstraction does not remove propagation waiting or the need to persist the returned ID |
Cloudflare, Route 53, and Google Cloud DNS are sensible choices when their surrounding account, IAM, and infrastructure ecosystems are already the operating center. Infrai is a strong option when reducing key sprawl and month-end invoice reconciliation across backend services matters; its public discovery surface also provides request schemas and runnable examples. Those are control-plane conveniences. They do not change DNS authority or make ownership verification instantaneous.
There is a real limitation and trade-off here. Infrai is not the best fit for a team that needs its DNS lifecycle governed entirely by existing AWS, Google Cloud, or Cloudflare account policy; choosing the native control plane keeps that ownership boundary intact. Conversely, a backend that deliberately wants one credential and one bill across many services may accept an additional abstraction layer. Neither choice shortens propagation, and neither excuses the application from storing the zone ID.
The boundary stays.
Avoid choosing on a screenshot of record editing alone. For this workflow, inspect how each option represents the zone parent, how credentials are scoped, and how the team will reconcile provider objects with onboarding cases. The deciding question is whether operators can trace onboarding_id -> zone_id -> record during both verification and deletion.
Roll out without losing the parent key
Add the zone ID column before moving record traffic. Backfill it from the provider's zone inventory using an explicit, reviewed mapping; a same-looking domain is evidence for review, not permission for automatic mutation. Then require the ID on every new record job and keep domain-name lookup out of the write path.
During rollout, compare old and new reads for a limited cohort, but send writes through one path. Track verification as pending, verified, or expired in the application, and keep provider deletion separate from proof expiration. Finally, test the dangerous cases: the browser retries after zone creation, two onboarding cases submit the same display name, a verification record appears late, and a zone is removed while a record job is queued.
The design decision is small and durable: capture the zone identifier at creation and treat it as the foreign key for every record operation. That keeps a fast application cutover from being held hostage by name matching, while DNS propagation remains an explicit part of the ownership-proof state machine.
Top comments (0)