TXT or CNAME for custom domain verification, and when each breaks

Three verification strategies with different failure modes. Which to pick for apex domains, wildcard subdomains and customers who won't delegate.

There are three ways to prove a customer controls a domain, and they fail differently. Full delegation — they point their nameservers at you — gives you the zone and POST /v1/dns/domain/add on Infrai returns the nameservers to hand them. A CNAME on a subdomain proves control of that subdomain only. A TXT record proves control without changing where traffic goes at all.

Most products need two of the three, because customers who will happily delegate app.customer.example will not delegate customer.example.

What each one actually proves

StrategyProvesCustomer changesBreaks when
Nameserver delegationcontrol of the whole zonenameservers at the registrarthey can’t delegate; corporate DNS says no
CNAME on a subdomaincontrol of that hostnameone recordapex domains — CNAME at the root is invalid
TXT recordcontrol of the nameone recordnothing; works at apex and subdomain
TXT + CNAME togethercontrol and routingtwo recordsmore steps, more typos

TXT is the most universal and the least useful on its own: it proves ownership but doesn’t route traffic, so a TXT-only flow still leaves you asking the customer for a second record before anything works — which is why products that start with TXT because it seems simplest end up with a two-step instruction page anyway, and why CNAME, which proves control and routes traffic in a single record, is the default for anything living on a subdomain.

Delegation gives you everything and asks the most.

Delegation: what the API hands you

curl -sS -X POST "https://api.infrai.cc/v1/dns/domain/add" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"domain": "northwind.example"}'
{
  "ok": true,
  "data": {
    "zone_id": "3a9d545444dc7a02e085889a3f713078",
    "domain": "northwind.example",
    "state": "pending",
    "name_servers": ["lilyana.ns.cloudflare.com", "margo.ns.cloudflare.com"]
  }
}

state: "pending" until delegation lands; name_servers are what the customer sets. Once it’s active you own every record in that zone, which is the strongest position — and the one many customers refuse, because delegating a zone means their own MX and internal records move too.

TXT verification, when they won’t delegate

If the customer keeps their own DNS, you still need a zone to write your own verification into — or you verify by asking them to publish a value you generate and then checking it. Where you do manage the zone, writing the record is an upsert:

curl -sS -X PUT "https://api.infrai.cc/v1/dns/record/upsert" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "zone_id": "3a9d545444dc7a02e085889a3f713078",
    "record_type": "TXT",
    "name": "_yourapp-verify.northwind.example",
    "content": "yourapp-verify=8f2c41bd7a9e",
    "ttl": 300
  }'
{
  "ok": true,
  "data": {
    "record_id": "rec_9wQ1zV6pLkS3dHyB",
    "zone_id": "3a9d545444dc7a02e085889a3f713078",
    "record_type": "TXT",
    "name": "_yourapp-verify.northwind.example",
    "content": "yourapp-verify=8f2c41bd7a9e",
    "ttl": 300,
    "priority": null,
    "proxied": false
  }
}

Two details that matter. Put the verification under an underscore-prefixed name — _yourapp-verify — so it can never collide with a hostname the customer wants to use. And use a short TTL while verifying: a 300-second TTL means a mistake is fixable in five minutes rather than a day.

The apex problem, stated plainly

A CNAME at the zone apex — northwind.example itself, with no subdomain — is invalid DNS. Not discouraged: invalid. If your product needs customers on their bare domain, CNAME verification can’t be your only strategy, and the alternatives are an A record pointing at a stable address you control, or full delegation.

That’s the constraint that usually decides the architecture.

Products living on app.customer.example can use CNAME everywhere. Products that must serve the bare customer.example need delegation or an apex A record, and an apex A record is a promise: that address has to stay stable for as long as the customer’s domain points at it, which rules out the convenient pattern of letting your edge addresses change with your infrastructure.

import os

import requests

API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})


def strategy_for(hostname: str) -> str:
    """Apex domains cannot use a CNAME. Deciding this in code, once, prevents the
    instruction page from telling a customer to do something invalid."""
    labels = hostname.strip(".").split(".")
    return "apex" if len(labels) <= 2 else "subdomain"


def plan(hostname: str, edge_target: str, edge_ip: str, zone_id: str) -> list[dict]:
    if strategy_for(hostname) == "subdomain":
        return [{"zone_id": zone_id, "record_type": "CNAME", "name": hostname,
                 "content": edge_target, "ttl": 300, "proxied": True}]
    return [
        {"zone_id": zone_id, "record_type": "A", "name": hostname,
         "content": edge_ip, "ttl": 300, "proxied": True},
        {"zone_id": zone_id, "record_type": "TXT", "name": f"_yourapp-verify.{hostname}",
         "content": "yourapp-verify=8f2c41bd7a9e", "ttl": 300},
    ]


def apply(records: list[dict]) -> list[dict]:
    out = []
    for record in records:
        resp = SESSION.put(f"{API}/v1/dns/record/upsert", json=record, timeout=30)
        resp.raise_for_status()
        out.append(resp.json()["data"])
    return out


if __name__ == "__main__":
    wanted = plan("northwind.example", "edge.yourapp.example", "203.0.113.10",
                  os.environ["ZONE_ID"])
    print([f"{r['record_type']} {r['name']}" for r in apply(wanted)])

Verify, then keep verifying

curl -sS -G "https://api.infrai.cc/v1/dns/record/list" \
  --data-urlencode "zone_id=3a9d545444dc7a02e085889a3f713078" \
  --data-urlencode "record_type=TXT" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

A domain that verified in March can stop being verified in September — the customer migrates registrars, an IT contractor cleans up “unused” records, a zone transfer drops what it didn’t understand. Re-check periodically with POST /v1/cron/create calling your own endpoint, and treat a formerly-verified domain that now fails as a notification rather than an outage: tell the customer before their site breaks.

Limitations

Zones added through this API live at Cloudflare, so delegation means delegating to Cloudflare’s nameservers. Some customers’ policies name a specific provider, and for them this isn’t a good fit — they keep their DNS and you verify by asking them to publish a value, which means your instruction page still exists even if most customers never see it.

The supported record types are A, AAAA, CNAME, TXT and MX, so CAA pinning or SRV records aren’t available here. And nothing tells you when a record you rely on disappears; the periodic re-check is yours to build.

The compensating half is that the next problem is already on the key. A verified custom domain almost always needs to send email from that domain, and POST /v1/email/domain/verify plus the SPF and DKIM records it wants go through this same PUT /v1/dns/record/upsert on the same credential — no second vendor, one bill, one GET /v1/account/usage. DNS routes report billing_class: free in discovery (verified 2026-09-21), and platform rates drift downward over time.

References

Browse more dns developer guides