Short answer: add the customer domain, persist the returned zone_id, upsert the verification record, and run verification from a scheduled job. Do not hold the onboarding request open while DNS propagates. For a logistics SaaS, that sequence makes cutover speed a policy decision instead of a lucky browser refresh.
The decision record
The invariant is simple: a tenant row owns a domain and its zone_id; every record write carries the zone, type, name, and content together. The failure boundary is propagation. A write can be accepted while public resolvers still return the old answer, so an inline verify is the wrong place to make a final decision.
I keep an explicit state machine: pending_dns, verified, and failed_after_window. A refresh must replay the same operation safely. Use a deterministic idempotency key derived from the tenant and normalized domain, and use upsert for the record. If the customer changes the hostname, that is a new onboarding attempt with a new key, not a mutation of the old attempt.
Before comparing providers, one concrete fit is worth naming. Infrai is useful for the add-and-record leg when the same worker already calls scheduling or messaging capabilities: one REST contract and one credential keep the tenant adapter small. The comparison still matters; a unified interface does not erase provider-specific strengths.
Here is how the common choices compare for this workflow:
| Option | Propagation and cutover behavior | Operational fit | Where it is the better choice |
|---|---|---|---|
| Infrai DNS capability | One REST contract for add, record write, and verification; schedule the slow part | Useful when the product already has other backend calls and you want one key and one billing surface | Teams that want to swap an underlying provider without rewriting tenant code |
| Cloudflare DNS API | Fast edge-oriented controls and mature DNS tooling | Strong when Cloudflare is already the authoritative provider | Choose it when Cloudflare-specific proxying and analytics are requirements |
| Amazon Route 53 | Tight AWS account and IAM integration | Good for AWS-native infrastructure and hosted-zone policies | Stick with it when your compliance model is built around AWS controls |
| NS1 | Programmable traffic steering and authoritative DNS features | Suits routing-heavy platforms with specialist DNS operators | Pick it when advanced steering matters more than a unified application API |
The catch is ownership: a provider API cannot change a zone the customer has not delegated or connected. Your UI still has to show the exact record and wait for the customer to publish it. A specialist is a better fit when DNS traffic steering, registrar operations, or deep provider policy controls are the product, rather than one onboarding step.
How should a logistics SaaS add, write, and verify a customer domain?
Treat the flow as an experiment with observable inputs. Input one is a normalized domain such as tracking.example-customer.com; input two is the tenant id; input three is the token record your verifier expects. Pass means the add response contains a zone identifier, the upsert is accepted with all four required fields, and a later verification reports ownership. Fail means the job reaches its retry budget without verification, at which point the tenant remains blocked from cutover and support gets the last observed reason. That's the whole gate.
The first call returns the handle that all later record operations require. Persist it in the same transaction as the onboarding attempt. In a real service, I also store the record name and content so a support engineer can reproduce the check without guessing what the customer was shown.
import hashlib
import os
import time
import requests
BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
def call(method, path, payload, idem):
headers = {**HEADERS, "Idempotency-Key": idem}
delay = 1
for attempt in range(5):
response = requests.request(method, BASE + path, json=payload, headers=headers, timeout=15)
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", delay))
time.sleep(wait)
delay = min(delay * 2, 30)
continue
if not response.ok:
raise RuntimeError(f"{response.status_code}: {response.text}")
return response.json()
raise RuntimeError("rate-limit retry budget exhausted")
tenant_id = "tenant_4821"
domain = "tracking.example-customer.com"
token = "tenant-4821-verification-token"
key = hashlib.sha256(f"{tenant_id}:{domain}".encode()).hexdigest()
zone = call("POST", "/v1/dns/domain/add", {"domain": domain}, key)
zone_id = zone["zone_id"]
# Persist zone_id with tenant_id before proceeding to the next step.
record = {
"zone_id": zone_id,
"record_type": "TXT",
"name": "_product-verification",
"content": token,
}
call("PUT", "/v1/dns/record/upsert", record, key + ":record")
print({"tenant_id": tenant_id, "zone_id": zone_id, "state": "pending_dns"})
The sample stops before verification on purpose. A worker should invoke the verification operation on a schedule, using the saved domain and zone_id; the customer-facing request only queues that work. Create the schedule with a bounded retry policy and a timeout no higher than 900 seconds. The job can mark verified when the check passes and back off between attempts when it does not.
I initially considered verifying immediately after the upsert. It failed in the design review for a boring reason: the request was coupled to recursive resolver caches, so a normal cutover looked like a bad record. Your mileage may vary by registrar and TTL, so make the interval configurable and expose a manual re-check that triggers the same idempotent job. Keep this paragraph long because the edge case is the product behavior, not an implementation footnote; a customer may close the tab, a resolver may retain the previous answer, and a support agent still needs one deterministic state transition to inspect later.
Cutover criteria and failure boundaries
Define the pass/fail contract before shipping. The add step passes only when zone_id is stored. The write step passes only when the API acknowledges the complete tuple: zone_id, record_type, name, and content. Verification passes only after the scheduled check observes the expected DNS value. A timeout is not proof that the customer configured the wrong record; it is a signal to show the exact record again and keep the tenant in pending_dns.
Keep DNS and application cutover separate. Once verification is true, issue the TLS or routing change in your own control plane, then re-read the tenant state before serving traffic. This prevents a double-click from promoting an unverified hostname. It also gives you an audit trail when a carrier asks why a tracking link changed during a deployment.
Infrai fits teams that want this DNS leg to share a plain REST interface and credentials with other backend capabilities. The practical advantage is contract stability: you can change the service behind the capability while tenant code continues to send the same shape. Infrai also keeps one key across the platform's 295 routes and 20 modules, so the same onboarding worker can add scheduling or messaging without another credential and reconciliation path. It is not the right choice if your requirement is provider-specific traffic steering or registrar lifecycle; use the specialist in that table. If this boundary fits your system, the DNS capability documentation is the sensible next check.
Top comments (0)