CustomDomain docs
Concepts

Setup types & the ask gate

How a domain is classified, the four connect rails, and the status-based edge authorization gate.

Setup types

POST /v1/domains:check classifies a domain into a setup_type that steers the widget to the smoothest available rail:

  • automatic — the provider can be configured without the customer copying records: an OAuth-into-provider authorization or a one-click setup apply. domains:check reports oauth_available and domain_connect so the widget knows which is on offer.
  • manual — no automatic path; the customer copies the exact records into their DNS provider and the poller watches for them.
  • async — a one-click setup flow that completes out-of-band (the provider applies the template after a redirect). This is also the rail that powers managed connections: start it with POST /v1/connections/{id}/domainconnect:start and {"managed": true} (or {"rail":"async"}) to get an async consent URL, and the callback (GET /v1/connect/dc/callback) exchanges the code for a durable, encrypted grant so the platform can re-apply and revert the template server-side later. When the provider does not support the async variant, the call falls back to the sync apply_url and the connection stays use-once.
  • mcp — reserved for agent-driven setup.

The engine also returns capability flags (ns_support, wildcard_support, cname_flattening, spf_override_support, caa_support) and any record_conflicts it observes in public DNS, so you can pre-warn the customer.

Rail truth: what to actually tell the user

setup_type: automatic means an adapter exists for the provider. It does not mean the end user won't have to type anything — for GoDaddy the adapter is an API key that GoDaddy only issues to accounts with 10+ domains or a paid plan. Read the rails block instead:

"rails": {
  "oauth":          { "available": false, "reason": "provider_has_no_oauth" },
  "domain_connect": { "available": false, "reason": "template_not_onboarded" },
  "api_key":        { "available": true,  "caveat": "GoDaddy only issues API access to accounts with 10+ domains or a paid plan; …" },
  "manual":         { "available": true }
},
"recommended_rail": "api_key",
"end_user_automatic": false,
"dashboard_url": "https://dcc.godaddy.com/control/acme.com/dns",
"nameservers": ["ns37.domaincontrol.com", "ns38.domaincontrol.com"]
  • rails.<rail>.available — can this rail run for this domain right now. When false, reason is one machine code: provider_has_no_oauth, oauth_client_not_registered (an operator task on your deployment), not_discovered, template_not_onboarded (the provider speaks Domain Connect but hasn't onboarded our template — GoDaddy, IONOS, Cloudflare), rail_disabled, no_adapter, unknown_provider.
  • caveat — a note on an available rail most users still can't use. Show it; don't treat it as unavailability.
  • recommended_rail — the server's pick, oauthdomain_connectapi_keymanual (least typing first). The console and the widget follow it, so all three surfaces agree.
  • end_user_automatic — true only when the recommended rail is OAuth or one-click. This is the fact supports_automatic was always misread as.
  • blocked_reason — present when no rail can connect the domain: unregistered (no nameservers — fix at the registrar) or platform_subdomain (www.<name>.vercel.app — the platform's DNS, not the user's; POST /v1/connections refuses it with the same code).
  • dashboard_url — a best-effort deep link into the provider's DNS panel, for the "add these records" step.

The legacy domain_connect boolean is derived from the same predicate as rails.domain_connect.available: one-click is advertised only where the provider is measured to serve our template (the DC_SYNC_PROVIDERS allowlist), never merely because discovery answered.

Control is proven by the rail

There is no separate ownership-TXT challenge. Each rail is itself proof that the customer controls the zone:

  • an OAuth authorization grants a one-time token to that customer's DNS account;
  • a one-click setup apply happens inside the customer's provider session; its async (managed) variant additionally leaves a durable, encrypted grant — the one credential the platform keeps, and only to re-apply or revert that same template;
  • an API token is a scoped credential only the zone owner can mint;
  • the manual flow requires the records to actually appear in the domain's authoritative DNS, value-checked.

For one-click setup specifically, the catalog ships a connect-verified template that points the apex A at the Customdomain edge and publishes the ownership TXT token together, applied atomically so serving and proof-of-control go live in one step (a verify-only template publishes just the token). The token record is flagged essential: OnApply, so applying the template that carries it is itself the ownership proof.

Optional credential reuse

By default an API-token credential is used once for the apply and then discarded. Optionally, when connecting in the console you can tick "Remember this key" to save that provider key for reuse. It is off by default and opt-in per key.

A remembered key is encrypted at rest (AES-256-GCM, scoped to one tenant + provider) and its plaintext never leaves the server. On the next connect at that provider, "Use saved key" applies records in one click without re-entering anything, and for Cloudflare / GoDaddy / Route 53 / Namecheap the same key can bulk-connect the account's other domains. Revoke a saved key any time under Settings → Connected provider accounts. See Provider setup and Security.

The ask gate

Before the edge serves TLS for a hostname, it calls POST /internal/ask. The control-plane approves the host iff a connection for it is live or propagating — a status-based gate, answered from a single indexed lookup so the TLS handshake never blocks on business logic. A domain the platform has never connected is refused, so the edge will never obtain a certificate for an unauthorized host.

On this page