# 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.

Source: https://docs.customdomain.ai/docs/connect-flow/offboarding

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:

```bash
curl https://api.customdomain.ai/v1/connections/<ID>/removal \
  -H "Authorization: Bearer <WIDGET_JWT or API key>"
```

```json
{
  "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

```bash
# 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:start` with
  `"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}:disconnect` with
  `{ "records": "remove", "provider": "cloudflare", "credential": { "token": "..." } }`.
  The key is used once and never stored.
* **Do it by hand**: remove the domain with `?records=keep` and follow
  `manual_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.disconnected` fires with a `removal` object: the choice, the
  outcome, the records removed, restored and left, and any manual steps. See
  the [event catalog](/docs/webhooks/overview#event-catalog).
* `GET /v1/connections/{id}/removal` keeps answering once the connection is
  gone: `removed: true` and `result`, what the removal did with the records.
  Read it, not the connection's `404`, 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](/docs/widget-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`.
