Apply records & go live
The three automatic rails, the manual fallback, propagation to live, and cert issuance.
Once a connection exists, get its desired records
into the customer's DNS via one of the four
rails. All of them move the
connection pending → propagating → live.
Rail C — OAuth into the provider (flagship)
curl -X POST http://localhost:8080/v1/connections/<ID>/oauth:start \
-H "Authorization: Bearer <WIDGET_JWT>" \
-H "Content-Type: application/json" \
-d '{"return_origin":"https://app.acme.example"}'
# → { "authorize_url": "https://<provider>/authorize?..." }The widget opens authorize_url in a popup. The provider redirects the customer
to GET /v1/connect/oauth/callback, which consumes the HMAC-signed single-use
state, exchanges the code, writes the records with the one-time access
token (discarded immediately, never stored), and returns an HTML page that
postMessages { type: "customdomain:oauth", payload } to the vetted
return_origin. The connection is now propagating.
Rail A — provider-hosted one-click setup
curl -X POST http://localhost:8080/v1/connections/<ID>/domainconnect:start \
-H "Authorization: Bearer <WIDGET_JWT>" \
-H "Content-Type: application/json" \
-d '{}'
# → { "apply_url": "https://<provider>/domainconnect/...", "service_id": "...", "dc_provider": "..." }Redirect the customer to apply_url; their provider applies the signed template.
Both the synchronous redirect and the asynchronous variant are supported.
service_id may be omitted when a single/default template is loaded.
Rail B — API token (BYO)
The customer supplies a scoped provider credential; it writes the records
once and is discarded afterwards by default (persisted only if the user opts
in to remembering it). In
the console, multi-part credentials are collected as separate labeled fields
(GoDaddy: API key + secret; Namecheap: username + API key; Route 53: access key
ID + secret access key) and joined into credential.token for you — see
Provider setup for each provider's exact steps.
Calling the API directly:
curl -X POST http://localhost:8080/v1/connections/<ID>/apply \
-H "Authorization: Bearer <WIDGET_JWT>" \
-H "Content-Type: application/json" \
-d '{
"provider": "cloudflare",
"zone": "customer.com",
"credential": { "token": "<scoped-provider-token>" },
"records": [ { "type": "CNAME", "host": "app", "value": "edge.customdomain.ai", "ttl": 3600 } ]
}'SPFM records are merged into any existing SPF TXT at write time (never
clobbered); mark a record essential: true to fail the apply if it can't be
written.
Bulk connect: more domains on one account
After a Rail B apply, one provider key can connect the other domains on that same account in a single step. In the console this is the "Connect more domains" panel: it lists the account's other zones and lets you Select all or pick a subset, then connects them without re-entering the key.
Listing is supported for Cloudflare, GoDaddy, Amazon Route 53, and Namecheap. Other providers can't enumerate an account's domains, so they degrade to connecting one domain at a time.
Two control-plane endpoints back it:
# 1. List the other zones on the account (credential passed once, not persisted)
curl -X POST http://localhost:8080/v1/providers/cloudflare/zones:list \
-H "Authorization: Bearer <WIDGET_JWT>" \
-H "Content-Type: application/json" \
-d '{"credential":{"token":"<scoped-provider-token>"}}'
# → { "provider": "cloudflare", "zones": ["acme.com", "beta.com"] }
# A provider that can't list returns 422 { "error": "list_not_supported", … }
# 2. Create connections for the selected domains (idempotent, up to 200)
curl -X POST http://localhost:8080/v1/connections:bulk \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{"domains":["beta.com","gamma.com"]}'
# → { "results": [
# { "domain": "beta.com", "connection_id": "con_…", "status": "connected" },
# { "domain": "gamma.com", "connection_id": "con_…", "status": "already" }
# ] }Each result carries a per-domain status: connected (a new connection),
already (one already existed — the call is idempotent, so re-running is safe),
or failed (that domain errored; the batch continues and reports its message).
Each new connection then applies records and goes live on its own rail exactly
like a single connect.
Manual
If no automatic rail is available, show the records from
GET /v1/connections/{id}/records for the customer to add by hand. Nothing else
is required — the poller watches for them.
Watch propagation → live
The control-plane's background poller (every minute) resolves each desired
record against public DNS and, when they all match their intended values, flips
the connection to live. On that transition the edge issues the TLS certificate
on the next handshake and a connection.live webhook
fires (exactly once). Treat webhooks as the primary signal and poll
GET /v1/connections/{id} as the fallback — delivery is durable but can lag
(the outbox retries for up to six hours).
Timing, honestly:
- A
pendingconnection (records not yet seen) never fails on its own. For the first 72 hours it is checked on a fast backoff; after that it stayspendingwitherror_code: setup_incompleteand is re-checked every six hours, forever. The moment the records appear it goes live. - A
propagatingconnection (a rail wrote the records) that never resolves within 24 hours becomesfailedwitherror_code: propagation_timeoutand firesconnection.failed(the event carrieserror_code/error_message). failedis not terminal.POST /v1/connections/{id}:recheckrevives it (fresh window, diagnosis cleared); re-POST /v1/connectionsfor the same domain revives the newest failed row and returns200; and real traffic arriving at the edge for a failed host re-enters it into verification automatically. Only a manual disconnect removes a connection.
Where a rail can complete today: one-click setup completes at NameSilo, Glauca
Digital and Domain Chief (the providers measured to serve our template
catalog); GoDaddy, IONOS and Cloudflare have not onboarded it, so they are
never offered one-click. Sign-in (OAuth) runs at Cloudflare, DNSimple, Netlify,
Vercel and WordPress.com, with a one-time token that is never stored. Read
rails on the pre-flight rather than memorizing this list — see
Rail truth.