CustomDomain Docs

Troubleshooting

Fix stuck connections and common failures: pending or propagating domains, DNS mistakes, rail errors, provider tokens, certificate issues and DNS drift.

Start with what the connection says

Every connection carries its own diagnosis: error_code and error_message on GET /v1/connections/{id}, on every list and on the webhook events. In the console, the page of a domain that isn't live leads with What happened and the next steps. Match the code or the symptom to a section below.

A connection is waiting in pending

pending means we have not yet seen the DNS records in public DNS. There is no deadline: a pending connection is re-checked every five seconds at first, backing off to every 15 minutes, and after three days every six hours, for as long as it takes. Add the records whenever you're ready and the domain connects on its own. After three days the console shows "We haven't detected the DNS records yet" so you know what's outstanding; that is a diagnosis, not a failure.

To check right now instead of waiting: Re-check now on the domain page (POST /v1/connections/{id}:recheck). It reports each record's verdict.

To see exactly what is wrong, open the domain's DNS tab: What public DNS shows right now (GET /v1/connections/{id}/diagnose) lists every record with the Name to type at the provider, whether public DNS has it, and what answers instead, naming the service when it recognizes it ("Still points to Squarespace"). It also flags the mistakes that block a connection even when a record looks right:

Mistakes that block a connection

  • The full hostname typed into the Name field. Most panels append the domain, so www.acme.com typed as the Name becomes www.acme.com.acme.com. Type only www (or @ for the root).
  • Cloudflare's proxy is on (orange cloud). Public DNS then shows Cloudflare's addresses instead of the record; switch the row to DNS only.
  • An old record is still there. A leftover A or AAAA (IPv6) record at the same name keeps sending some visitors to the previous host.
  • A conflicting record at the same name (a CNAME cannot sit beside other records) or a CAA record that doesn't allow Let's Encrypt.

The widget runs the same check for your customers while they add records.

Common reasons the records never show up:

  • The record was added at the wrong place. Records must be added at the provider that serves the domain's nameservers (dig NS your-domain.com), which is not always the registrar you bought the domain from.
  • Root domain on GoDaddy, IONOS or OVH. These panels have no ALIAS/ANAME type, so the console shows A records to our apex addresses instead of a CNAME. Add exactly those A records at @. (A CNAME at @ is rejected by the panel, or breaks your email.)
  • The domain doesn't resolve at all: unregistered, or registered but never delegated to a DNS provider. The connect flow now refuses these up front with an explanation; fix the delegation at your registrar first.
  • A hosting platform's subdomain (something.vercel.app, *.netlify.app, *.github.io, …) can't be connected: the platform owns its DNS. Use a domain you control.

My domain points at CustomDomain™ but shows a connection / certificate error

If you added the DNS records after the connection had been marked failed, visitors' browsers reach our edge but the connection isn't serving yet. The edge treats that traffic as your signal: the connection is re-entered into verification automatically and goes live on the next check (usually within a minute or two). You can also press Re-check now. If it stays failed, the domain already has another active connection in the same application; finish or delete that one.

What "failed" means now

A connection only reaches failed when records we wrote ourselves never propagated (24h), or a credentialed write was rejected. Never because you were slow. Finish setup on the domain page resumes a failed connection (same row, history kept); adding the same domain again does the same thing rather than creating a duplicate.

Verification links last one hour and work once. On the Check your email screen use Resend the link; an expired link lands on a page where you can request a new one. Still nothing? Check spam, then email [email protected] and we'll verify you by hand.

GoDaddy won't give me an API token

Any GoDaddy account that holds at least one domain can create a Personal Access Token. Open GoDaddy's Personal Access Token page, click + Generate Token, tick only domains.domain:read and domains.dns:update, set a short expiration and copy the token (GoDaddy shows it once). An older key and secret pair still works, but GoDaddy has deprecated it for the Domains API. If you would rather not create a token, use secure sign-in where it is offered: you sign in to GoDaddy and it makes the token for you. Or add the records shown in the console by hand; that works on every GoDaddy account. See GoDaddy setup. GoDaddy's one-click setup (Domain Connect) isn't available yet: it is waiting on GoDaddy to onboard our templates.

Route 53: "The security token included in the request is invalid"

You pasted temporary credentials (an access key starting with ASIA…). Those need the third field, Session token, which the Route 53 form now has. Permanent IAM-user keys (AKIA…) don't need it.

A connection is stuck in propagating

Records were written but aren't all resolving to their intended values yet.

  • It is rarely propagation itself. When public resolvers still hold an old answer, we ask the zone's own nameservers, and every one that answers must serve the value we expect. A provider still copying the change between its nameservers takes a moment; one that never does points at the records.
  • Verify the records at the provider's dashboard: a rail write can succeed at the API but land on the wrong host, or a customer may have edited them afterward.
  • Visitors whose resolver cached the old answer can still reach the old host until that record's TTL runs out, even after the connection is live.
  • propagating connections that never resolve within 24 hours transition to failed and fire connection.failed.

service_unavailable on a rail

POST /v1/connections/{id}/apply (and the OAuth / one-click setup starts) return 503 service_unavailable when the corresponding engine isn't wired for your plan or environment: no DNS provider registry, no OAuth client for the provider (OAUTH_<PROVIDER>_CLIENT_ID/_SECRET), or no one-click signing key (DC_SIGNING_KEY_FILE). Contact support if you hit this on the hosted service.

Rail B write failed

apply returns an error carrying the provider's message when the scoped token is wrong-scoped, expired, or the zone doesn't match. Confirm the token has DNS edit rights on the exact zone you passed, and mind provider gates (Namecheap's account eligibility, the IP allowlists at Namecheap, Dynadot, Epik, NameBright, Internet.bs and Vultr, and at NS1 when its account or key has one; a netcup domain on CloudDNS needs the API key alone, not the customer number and password pair); see Provider setup.

A sign-in or one-click popup never reports back

  • PopupBlockedError: the browser blocked the authorize window; trigger oauth:start from a direct user gesture.
  • The OAuth callback, and the one-click return page (GET /v1/connect/dc/return/{state}), only postMessage to a return_origin on the server's allowlist (OAUTH_ALLOWED_RETURN_ORIGINS); a mismatched origin silently drops the result.
  • An expired or replayed state returns 400 from the callback; re-run oauth:start to get a fresh one.
  • Either way the connection keeps being checked, and it goes live once public DNS shows the records, whether or not the page heard back.

A live domain stopped working

The lifecycle doesn't demote a live connection, but Monitor fires domain.record_missing when a live domain's records stop resolving (where drift alerts are switched on; the hosted service has them off by default, so no such webhook is sent there yet). Have the customer restore the records (or re-apply them via a rail). Run POST /v1/monitor:check to compare on demand.

Certificate not issuing

Certificates are issued on demand at the TLS handshake, gated by POST /internal/ask returning { approved: true }, which happens only once the connection is propagating or live. If the edge can't reach the gate, check EDGE_ASK_URL, EDGE_ASK_SECRET, and EDGE_ACME_PROD/EDGE_ACME_EMAIL.

401 "unknown application or secret" when minting a widget token

POST /v1/tokens answers 401 with unknown application or secret when the application_id does not exist or the client_secret does not match it. The usual cause is an API key in the client_secret field. An API key (cd_live_…, cd_test_…, or an older sk_live_…) and an application client secret (sk_ plus 32 hex characters) are different credentials, and the key never works there.

  • You signed up in the console (most customers): you have no client secret, because the console never shows it. Call POST https://app.customdomain.ai/api/widget-token with Authorization: Bearer <API key> instead. Create the key on API & SDK; the Install tab on Widget generates the code. See Widget tokens.
  • You created the application over the API: use the client_secret from that creation response. Check the application_id belongs to the same application. If the secret is lost, rotate it with POST /v1/applications/{id}/client-secret:rotate.
  • Do not rotate a console-created application's secret. The console keeps its own copy, which would no longer match: its widget preview, Try it live and /api/widget-token then fail for that application.

The console endpoint has its own errors: 503 no_widget_credentials means the application was not created in the console (mint with /v1/tokens and its secret), and 403 forbidden means the application_id is not in the key's workspace.

"A card is needed to create API keys" (402 billing_required)

A new workspace can look around, check domains and design its widget, its first ten domains are free, and it has a default key from the start, but it needs a card before it makes a new credential: another API key, an application's client secret or a new application. Creating one before then returns 402 billing_required. An owner or admin adds a card under Billing (the 14 day free trial starts; nothing is charged until it ends), then creates the key again.

The widget says the session ended

Widget tokens last 15 minutes by default (see Widget tokens), and adding records by hand often takes longer. Pass getToken to open() and the widget fetches a fresh token and carries on. Without it, the widget tells the user they can close the window: the connection keeps being checked on the server and goes live on its own once the records appear.

On this page