# Custom domain

_Category: REST API reference_

A custom domain puts your help center on your own address, such as `help.acme.example`. Set the domain, add the DNS record the answer gives you, and follow the domain's checks until it is live. These endpoints manage the same domain as **Custom domain** in Settings (see [Use your own domain](https://self.helpcenter.io/content/custom-domain)).

## Endpoints

| Method and path | What it does | Scope |
| --- | --- | --- |
| `GET /v1/custom-domain` | Get the custom domain and its checks | `domain.manage` |
| `PUT /v1/custom-domain` | Set the custom domain | `domain.manage` |
| `DELETE /v1/custom-domain` | Remove the custom domain | `domain.manage` |

All three act on the help center of the credential: there is no help center id in the path. An API key holds `domain.manage` only when one of the help center's Owners or Admins has ticked **Allow managing the custom domain** for it in Settings, under **API keys**: when they create the key, or later with **Edit access** on it, which keeps the key and applies from its next request. Only a **Read & write** key can hold it. Any other key gets `403 Forbidden` with `insufficient_scope` on all three, `GET` included, and the message names that setting. A key works only while the person who created it is an Owner or Admin of the help center, the people who may change its domain in the dashboard.

An OAuth app asks for `domain.manage`, and the consent screen shows the person "Connect your help center to your own domain, check its DNS and certificate, and remove the domain." The token then works only while that person is an Owner or Admin of the help center: anyone else gets `403` with `role_forbidden`. An OAuth token names its help center as shown in [Choose which help center a request acts on](https://developers.helpcenter.io/content/choosing-a-help-center). The setting under API keys is for API keys only: a token holds the scopes its person approved. See [Scopes](https://developers.helpcenter.io/content/scopes).

## How a domain goes live

1. **Set the domain** with `PUT /v1/custom-domain`. `dns_records` holds the one record the domain needs, and `status` is `pending_dns` until that record is in place.
2. **Add the record** at the domain's DNS provider, exactly as `dns_records` gives it: a `CNAME` from the domain to the help center's helpcenter.io address.
3. **Follow the checks.** HelpCenter.io checks the domain every few minutes and moves it one step at a time: `pending_dns`, `verifying`, `issuing_certificate`, then `live`. Read `GET /v1/custom-domain` every five minutes or so. `live` comes some time after the record appears, not at once.

`live` means the domain reaches your help center. Readers see its pages there once an Owner or Admin publishes the help center in Settings: until then, every address of it, the custom domain included, answers a page saying it isn't published yet, and only its team can open it.

During the first day after you set a domain, HelpCenter.io looks for its record every few minutes; after that, hourly, and after the first week, daily. Add the record soon after you set the domain. These checks run only for a help center on a trial or on a subscription that hasn't been cancelled; for any other, the domain stays at the step it has reached.

### The DNS record

- **A plain CNAME at a subdomain,** such as `help.acme.example`. The API accepts a root domain such as `acme.example`, but most DNS providers can't put a CNAME there, and a flattened root record shows no CNAME: such a domain stays `pending_dns`.
- **DNS only, not proxied.** A record behind a proxy, such as Cloudflare's orange cloud, hides the CNAME, and the domain never shows as pointing here. See [Set up your custom domain with Cloudflare](https://self.helpcenter.io/content/custom-domain-cloudflare).
- **The name is the full domain.** `name` is `help.acme.example`; many DNS providers want only the part before your domain (`help`) in their name field.
- **Keep the record.** If it is removed, `status` goes back to `pending_dns`.

## The custom domain object

| Field | Type | Description |
| --- | --- | --- |
| `domain` | string or null | The domain, as stored. `null` when none is set. |
| `status` | string or null | Where the domain stands, in the table below. `null` when none is set. |
| `dns_records` | array | The record to add: one entry with `type` (`CNAME`), `name` (the domain) and `value` (the help center's helpcenter.io address). Empty when no domain is set. |
| `certificate` | object or null | `status`: `waiting_for_dns`, `requested`, `issued`, `not_requested` or `failed`. |
| `checks` | object or null | Three booleans: `dns_points_here` (the domain's CNAME names this help center), `certificate_issued` and `certificate_serving` (the certificate serves the domain, checked once the first two are `true`). |
| `failure` | object or null | With `status` `failed`: a `code` and a `message` to pass on to the help center's owner. |
| `last_checked_at` | string or null | When the checks ran, ISO 8601. |

The answer doesn't say how many domain changes are left this month.

### Statuses

| `status` | Meaning |
| --- | --- |
| `pending_dns` | The domain's CNAME doesn't point at the help center yet, or no longer does. |
| `verifying` | The record points here. A next check requests the certificate. |
| `issuing_certificate` | The certificate is being issued, or is issued and not serving the domain yet. |
| `live` | The record points here, and the certificate is issued and serving the domain. |
| `failed` | `failure` says why. |

| `failure.code` | `failure.message` |
| --- | --- |
| `domain_in_use` | `This domain is already connected to another help center. If that help center is yours, remove the domain there first; otherwise contact support.` |
| `not_requested` | `No certificate has been requested for this domain, usually because it was saved after this month's 4 domain changes. Remove it and set it again once the limit allows.` |
| `certificate_failed` | `The certificate for this domain could not be issued. Check the DNS record, then remove the domain and set it again, or contact support.` |

Setting the same domain again doesn't retry a failed one, because it changes nothing. Follow the message: remove the domain, then set it again, which can count as one of the month's changes.

## Get the custom domain

GET`/v1/custom-domain`

Get the help center's custom domain, the DNS record it needs, and where its checks stand.

Reading never moves a domain along: it reports the latest checks. A read can reuse checks up to a minute old, and `last_checked_at` says when they ran. The answers to `PUT` and `DELETE` always carry fresh checks.

## Set the custom domain

Send the domain. The answer is its status, with the record to add.

PUT`/v1/custom-domain`

Set the help center's custom domain. Setting the domain it already has changes nothing; another domain replaces the current one at once.

| Field | Type | Description |
| --- | --- | --- |
| `domain` | string | Required. A host name of up to 253 characters, such as `help.acme.example`: no `https://`, path, port or trailing slash. Labels hold letters, digits and hyphens; send an internationalized name in its ASCII (`xn--`) form. |

- **The same domain changes nothing.** Setting the domain the help center already has, in any letter case or with a `www.`, answers its status and leaves it as it is, so a retry is safe.
- **Another domain replaces the current one at once.** The help center stops answering on the old domain right away, before the new one is live. Replace a live domain only when the help center's owner wants it moved.
- **How it is stored.** Every leading `www.` is removed, in any letter case, so `www.help.acme.example` is stored as `help.acme.example`. A `www.` further in is part of the name and is kept: `www.docs.www.acme.example` is stored as `docs.www.acme.example`. Otherwise letter case is kept as you send it: send the domain in lower case.
- **4 changes in a rolling month.** Each new domain you set counts. Setting the domain the help center already has doesn't, and neither does removing it. A fifth change answers `429 Too Many Requests` with `custom_domain_limit_reached` and changes nothing; its `Retry-After` header gives the seconds until the oldest change in the count is a month old.

The refusals, in the order they are checked:

- `422` with `validation_error` for a value that isn't a host name, with `Invalid domain name. Please do not include http(s):// or trailing slash. Multiple subdomain levels are supported.`, and for a helpcenter.io address, with `This is not a custom domain: .helpcenter.io addresses are managed automatically.` A missing `domain` answers `The domain field is required.`, and one that isn't text, such as a list, `The domain field must be a string.` followed by the invalid-name message.
- `403` with `custom_domain_not_in_plan` when the help center's plan doesn't include a custom domain: `This help center's plan does not include a custom domain. Its owner can upgrade on the billing page.` The body's `upgrade_url` is that billing page.
- `409 Conflict` with `custom_domain_busy` when another change to this help center's domain, or to the same domain, is still running: `Another change to this help center's custom domain, or to this domain, is still running. Retry in a few seconds.`
- `409` with `domain_taken` when another help center uses the domain. The message names no help center. If the other help center is the owner's, they remove the domain there first.
- `429` with `custom_domain_limit_reached`, above.

## Remove the custom domain

The help center stops answering on its custom domain at once, and keeps its helpcenter.io address. Removing doesn't count toward the month's changes.

DELETE`/v1/custom-domain`

Remove the custom domain. With none set, the answer is the same.

With no custom domain set, the answer is the same `200` with `"domain": null`, so a retry is safe. While another change is running, the answer is `409` with `custom_domain_busy`.

## With an AI agent's key

An AI agent's key reads, sets and removes the custom domain only once the help center it created is claimed and its owner has verified their email address and activated a plan. Until then, every call answers `403` with `capability_locked`, the conditions it still `requires` and a `next_step` to pass on to its user. A key its user lets it keep at the claim holds the custom-domain setting from then on; if the user unticks **Allow managing the custom domain** for it, all three answer `403` with `insufficient_scope`. While a migration request holds the help center, setting and removing answer `409` with `migration_in_progress`. The whole flow is in [Migrate a knowledge base as an AI agent](https://developers.helpcenter.io/content/migrate-a-knowledge-base-as-an-ai-agent).

## Errors

| Status | Code or message | When |
| --- | --- | --- |
| `401` | `"status": "unauthorized"` | No key or token, or one the API doesn't accept. |
| `403` | `insufficient_scope` | A key nobody ticked **Allow managing the custom domain** for, a Read only key, or an OAuth token without `domain.manage`. A key is told `This action requires the domain.manage scope. An API key holds it only when it is a Read & write key and one of the help center's Owners or Admins has ticked "Allow managing the custom domain" for it in Settings > API keys.` A token gets the first sentence only. |
| `403` | `role_forbidden` | An OAuth token whose person isn't an Owner or Admin of the help center: `Your role on this help center ({role}) does not allow this action, which needs the domain.manage permission there. Ask one of its admins to change your role.` The body's `role` names their role. |
| `403` | `custom_domain_not_in_plan` | Setting a domain on a plan without one. `upgrade_url` is the billing page. |
| `409` | `domain_taken` | Another help center uses the domain. |
| `409` | `custom_domain_busy` | Another change to the domain is still running. Retry in a few seconds. |
| `422` | `validation_error` | A missing or invalid `domain`, or a helpcenter.io address. |
| `429` | `custom_domain_limit_reached` | A fifth domain change in a rolling month. Wait `Retry-After` seconds. |
| `429` | `{"message": "Too Many Attempts."}` | A rate limit. Wait `Retry-After` seconds. |

See [Errors](https://developers.helpcenter.io/content/errors) for the shapes every endpoint shares, and [My custom domain isn't working](https://self.helpcenter.io/content/custom-domain-not-working) for DNS problems.

## Related

- [Redirects](https://developers.helpcenter.io/content/redirects-api)
- [Migrate a knowledge base as an AI agent](https://developers.helpcenter.io/content/migrate-a-knowledge-base-as-an-ai-agent)
- [Scopes](https://developers.helpcenter.io/content/scopes)
- [Use your own domain](https://self.helpcenter.io/content/custom-domain)
