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.comtyped as the Name becomeswww.acme.com.acme.com. Type onlywww(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
AorAAAA(IPv6) record at the same name keeps sending some visitors to the previous host. - A conflicting record at the same name (a
CNAMEcannot sit beside other records) or aCAArecord 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.
The verification email never arrived / the link expired
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. propagatingconnections that never resolve within 24 hours transition tofailedand fireconnection.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:startfrom a direct user gesture. - The OAuth callback, and the one-click return page
(
GET /v1/connect/dc/return/{state}), only postMessage to areturn_originon the server's allowlist (OAUTH_ALLOWED_RETURN_ORIGINS); a mismatched origin silently drops the result. - An expired or replayed
statereturns400from the callback; re-runoauth:startto 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-tokenwithAuthorization: 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_secretfrom that creation response. Check theapplication_idbelongs to the same application. If the secret is lost, rotate it withPOST /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-tokenthen 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.
Notifications
See which CustomDomain™ notifications reach the console bell and browser push, like domains going live or certificate problems, which emails we send and who gets them, and how to turn each off.
FAQ
Short answers to common questions about connecting domains: which providers connect automatically, how records get written, whether keys are stored, and apex domains.