# Setup types & the ask gate

> How a domain is classified as automatic or manual, how async one-click setup fits in, what proves control for each rail, and how the ask gate guards the edge.

Source: https://docs.customdomain.ai/docs/concepts/ownership-and-setup-types

## Setup types

`POST /v1/domains:check` classifies a domain into a `setup_type` that steers the
widget to the smoothest available [rail](/docs/connect-flow/overview#the-four-rails):

* **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.

`setup_type` is always `automatic` or `manual`. The async flow below is not a
setup type but a way to start the one-click rail:

* **async one-click setup**: a one-click flow that completes out-of-band (the
  provider applies the template after a redirect). This is also the rail that
  powers [managed connections](/docs/connect-flow/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.

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 needs
a Personal Access Token the user creates at GoDaddy. Read the `rails` block
instead:

```json
"rails": {
  "oauth":          { "available": false, "reason": "provider_has_no_oauth" },
  "domain_connect": { "available": false, "reason": "template_not_onboarded" },
  "api_key":        { "available": true,  "caveat": "Needs a GoDaddy Personal Access Token: any account that holds a domain can create one …" },
  "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&#x2A; domain right now.
  When false, &#x2A;*`reason`** is one machine code: `provider_has_no_oauth`,
  `oauth_client_not_registered` (the provider has a sign-in but this
  deployment has not switched it on: an operator task; on the hosted service
  this is the answer for OVHcloud and for every sign-in through a provider's
  MCP server except Dynadot's and Unstoppable Domains' for now),
  `not_discovered`, `template_not_onboarded`
  (the provider speaks Domain Connect but hasn't onboarded our template:
  GoDaddy, IONOS, Cloudflare, Vercel), `redirect_unavailable` (the provider
  needs our return link for one-click setup, and this deployment cannot send
  one), `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, `oauth` → `domain_connect` →
  `cloud_browser` (the secure sign-in) → `api_key` → `manual` (least typing
  first). The secure sign-in leads a pasted API key wherever it makes the key
  itself (`rails.cloud_browser.key` is then set and names what the key can do
  and when it ends), or the key way is unavailable or carries a caveat. An
  API key caller whose workspace already keeps a key that can act for the
  domain gets `saved_access` first, with `rails.saved_access` listing the keys;
  an end user's widget token never gets it. The
  console and the widget follow it, within the ways each can show, so the
  surfaces agree.
* **`rails.nameservers`**: when the application offers DNS hosting, the verdict
  on it for this domain: `offered`, `offered_first` (the application's setting
  is `prefer`), `recommended` (no automatic way works) or `unavailable`, with a
  `reason`. Absent while the setting is off.
* **`also_provider`**: set when the registrar and the nameservers name two
  providers. A domain that was bought at Google Domains is Squarespace's now
  and keeps Google's nameservers, which Google Cloud DNS zones use too: `provider`
  is `squarespace` (so its secure sign-in is offered) and `also_provider` is
  Google Cloud DNS.
* **`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`, which is 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 serves our template, never merely because discovery answered: before
offering one-click, the server asks the provider's own API about the exact
template the start would send (the answer is cached for five minutes), so the
button appears by itself once a provider onboards our template. A deployment
can also allowlist providers with `DC_SYNC_PROVIDERS`.

## Control is proven by the rail

On the hosted service there is **no** separate ownership-TXT challenge (a
deployment can switch an ownership challenge on with `DOMAIN_AUTHORITY_ENABLED`;
it is off by default). Separately, a domain that another account already serves
is never promoted to `live` for a second account (`domain_already_connected`).
Each rail is itself proof that
the customer controls the zone:

* an **OAuth** authorization grants a token for that customer's DNS account
  (used once, or kept as a
  [managed sign-in grant](/docs/connect-flow/managed-connections#managed-sign-in-grants));
* a **one-click setup** apply happens inside the customer's provider session; its
  async ([managed](/docs/connect-flow/managed-connections)) variant additionally
  leaves a durable, encrypted grant, used only to re-apply or revert that same
  template;
* a **secure sign-in** happens in the customer's own session at the provider, and
  the changes can be made only in the account that holds the domain: a sign-in to
  an account that does not hold it changes nothing and the customer is asked to
  use another ([signed in to the wrong
  account](/docs/guides/secure-sign-in#signed-in-to-the-wrong-account));
* an **API key** 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;
* **DNS hosting** is the owner's own change of the domain's nameservers at its
  registrar, and the zone turns active only when the registry lists every one of
  them as ours ([Managed DNS](/docs/dns/managed-dns#how-it-works)).

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

Through the API, an API-token credential is used for the apply and then
discarded; connect it as a
[provider account](/docs/connect-flow/provider-accounts) to keep it. In the
console, **Remember this key for my &#x2A;\<provider>* account** starts ticked,
so the key is kept as a provider account unless the user unticks it.
Account passwords and tokens that expire within hours are never saved.

A remembered key is encrypted at rest (AES-256-GCM) and its plaintext never
leaves the server. On the next connect at that
provider, &#x2A;*"Use saved key"** applies records in one click without re-entering
anything, and for Cloudflare, GoDaddy, Route 53, Namecheap, IONOS,
united-domains, Epik, NameBright, cPanel hosts and other API key providers that
can list the zones a key covers, the same key can
[bulk-connect the account's other domains](/docs/connect-flow/verify-and-go-live#bulk-connect-more-domains-on-one-account).
Revoke a saved key any time under **Connected accounts** in the console sidebar. See
[Provider setup](/docs/dns/provider-setup#remembering-a-key-optional) and
[Security](/docs/security/overview#provider-credentials).

## The `ask` gate

Before the edge serves TLS for a hostname, it calls `POST /internal/ask`. The
control-plane approves the host &#x2A;*iff a connection for it is `live` or
`propagating`**, it has not been removed, and its record set includes an edge
`CNAME`/`ALIAS` or the platform's `A` records (otherwise the answer carries
`reason: no_edge_record`). It is a status-based gate, answered from a lookup of
the connection 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.
