Removing a domain
Remove a connected domain and choose what happens to the DNS records CustomDomain™ added: take them out and restore what they replaced, keep them, or go manual.
Removing a domain always stops serving it, stops its certificate from renewing and stops monitoring it. What happens to the DNS records CustomDomain™ added is a choice: remove them (and put back what each one replaced) or keep them.
1. Read the plan
GET /v1/connections/{id}/removal says what removal can do, and changes
nothing:
curl https://api.customdomain.ai/v1/connections/<ID>/removal \
-H "Authorization: Bearer <WIDGET_JWT or API key>"{
"domain": "shop.acme.com",
"connected_via": "sign_in",
"access": "one_time",
"record_removal": "reauthorize",
"default_choice": "keep",
"records": [
{ "type": "CNAME", "name": "shop", "value": "edge.customdomain.ai",
"change": "replaced", "prior": [ { "type": "CNAME", "value": "shops.oldhost.example" } ],
"action": "restore" }
],
"reauthorize": { "methods": ["sign_in", "api_key"], "provider": "cloudflare", "flow": "oauth2" },
"manual_steps": [ "At Cloudflare, change the CNAME record at \"shop\" from ... back to ..." ],
"teardown": [ "The domain stops being served through CustomDomain™. ..." ]
}Every write records what it did to each name (change: created, added,
replaced, merged) and the values it replaced (prior), read from the
provider or public DNS right before the write. Removal only touches values
that are still ours: a record someone changed since is left and reported.
record_removal depends on the access CustomDomain™ still holds:
| How it was connected | Access held | record_removal |
|---|---|---|
| Managed DNS (the zone we host) | continuous | automatic |
| Kept sign-in, continuous provider account | continuous | automatic |
| Kept Domain Connect authorization | continuous | automatic (partial when a value must be put back) |
| Route 53 access role | continuous, writes only | partial (deletions by hand) |
| API key or sign-in that was not kept | one time | reauthorize: one more sign-in or key, or the manual steps |
| Domain Connect one-click, manual records | none | manual |
| Another application still serves the domain | in_use: every record stays |
2. Remove
# Remove the records (and put back what they replaced) where access allows
curl -X DELETE "https://api.customdomain.ai/v1/connections/<ID>?records=remove" \
-H "Authorization: Bearer <WIDGET_JWT or API key>"
# Keep the records
curl -X DELETE "https://api.customdomain.ai/v1/connections/<ID>?records=keep" ...With records the answer is 200 and says what happened to each record
(removed, restored, left_in_place, failed, manual_steps). Without it
the call keeps its original contract and answers 204: the records are taken
out only through a kept Domain Connect authorization, a kept sign-in or a zone
CustomDomain™ hosts, and kept otherwise (a connected provider account or the
Route 53 access role never write on a DELETE without a choice).
A removal never touches a record whose value changed since it was written, a
value that was already there before CustomDomain™ wrote it, or a record another
connected domain still uses (a www record a root domain's redirect and a
www connection share, a CAA record several subdomains rely on). A key or a
sign-in must reach the zone that holds the records: when it shows none of them
(it may belong to another account), the answer lists the manual steps instead
of claiming anything was removed.
When removing the records needs access that was not kept, ?records=remove
answers 409 reauthorization_required with the plan. Then either:
- Sign in once more:
POST /v1/connections/{id}/oauth:startwith"purpose": "remove". The provider sign-in removes the records and the domain, and its result carries"purpose": "remove"and the outcome. The sign-in is used once and never kept. - Paste a key:
POST /v1/connections/{id}:disconnectwith{ "records": "remove", "provider": "cloudflare", "credential": { "token": "..." } }. The key is used once and never stored. - Do it by hand: remove the domain with
?records=keepand followmanual_steps.
While a removal runs, a second one answers 409 removal_in_progress.
A delegated agent token can remove a domain too, when its grant carries
domains:disconnect and the connection is in its application. It must send
confirm=<domain>: without it the answer is 400 confirmation_required (its
details name the domain), and with another domain 409 confirmation_mismatch.
Nothing is removed in either case.
Any other credential (an API key, an application's client credentials token, a
widget token) may leave confirm out, and the removal keeps its original
contract. One that sends it is held to it the same way: another domain is
409 confirmation_mismatch, an empty value is 400 confirmation_required, and
nothing is removed. Case, a scheme, a path and a trailing dot do not matter. The
MCP server's disconnect-domain always sends it, whatever the credential.
What always happens
- The edge stops approving the host at once and every edge forgets it within a
minute (
GET /internal/removed-hosts), so the certificate stops renewing and is deleted from the edge cache. - Monitoring stops.
connection.disconnectedfires with aremovalobject: the choice, the outcome, the records removed, restored and left, and any manual steps. See the event catalog.GET /v1/connections/{id}/removalkeeps answering once the connection is gone:removed: trueandresult, what the removal did with the records. Read it, not the connection's404, to learn the outcome.
From the widget
customdomain.removeDomain({ applicationId, token, connectionId }) (or
prefilledDomain in place of connectionId) opens the same flow for your end
user: the records, the choice, one more sign-in when needed, and a summary with
anything left to delete by hand, in their language. The outcome arrives in
onRemoved and the customdomain:removed event. See the
SDK reference.
Mint the widget token on your server for that end user and that domain:
POST /v1/tokens with domain and end_user_ref (your own id for the end
user). An end user's token that is not bound to the domain, or bound to another
end user than the one the connection names, cannot remove it. A connection
created with a token that carries end_user_ref is stamped with it.
Finding the connection
An app that stored the domain but not the id can call
GET /v1/connections:lookup?domain=shop.acme.com.
Monitoring and services
Every live domain is checked hourly against its records. Drift is confirmed first, and records CustomDomain™ wrote are put back where it has continuous access.
How to connect a custom domain
Connect a custom domain to your SaaS product: which records you need on a subdomain or the root, why certificates fail separately, and how long it really takes.