# Troubleshooting

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

Source: https://docs.customdomain.ai/docs/troubleshooting

> **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*&#x2A;: 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 &#x2A;"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.

## 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
[support@customdomain.ai](mailto:support@customdomain.ai) 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](https://developer.godaddy.com/en/personal-access-token&#x29;,
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](/docs/guides/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](/docs/dns/provider-setup#godaddy). 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](/docs/dns/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](/docs/webhooks/overview#event-catalog) 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](/docs/authentication/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](/docs/authentication/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.
