---
title: "Add a custom domain and start connecting it"
method: POST
path: "/api/v1/sites/{slug}/custom-domains"
tags: ["Domains"]
---

# Add a custom domain and start connecting it

`POST /api/v1/sites/{slug}/custom-domains`

## Request

- Base URL: `https://api.dotsy.ai`
- URL: `https://api.dotsy.ai/api/v1/sites/{slug}/custom-domains`
- Auth: HTTP bearer

Adds one hostname, or an apex and its `www` together. A nested subdomain of the platform's own site domain is connected at once (200 if it was already on this site, otherwise 201). An external domain needs a paid plan and starts a connect session: the 201 body carries `connect`, which says what to do next (open a link, or enter DNS records). Re-sending a host already on the site is safe and returns it again without using another slot. Each site and each account has a cap on external hostnames (`custom_domain_limit`). Then poll refreshCustomDomain until `status` is `connected`. Needs the site owner. Editors, viewers, guest tokens (`guest_requires_account`) and keys scoped to other sites are refused.

## Path parameters

- `slug` string, required

## Request body

- AddCustomDomainRequest: Send `hosts` (or `host`). One of them is required.
  - `hosts` string[]: One or two hostnames. Two is only valid as an apex and its `www` together (they share one connect session). Takes precedence over `host`.
  - `host` string: A single hostname — the same as `hosts: [host]`.

## Response `200`

That internal host is already on this site.

- CustomDomainResponse
  - `domain` CustomDomain, required
    - `host` string, required
    - `slug` string, required: The site this hostname serves.
    - `kind` 'internal' | 'external', required: `internal`: a nested subdomain of the platform's own site domain, connected at once with no DNS. `external`: a domain you own, which needs DNS records and a certificate.
    - `status` 'pending_dns' | 'pending_cert' | 'connected' | 'error', required: `pending_dns`: waiting for the DNS records. `pending_cert`: DNS is right, the HTTPS certificate is being issued. `connected`: live. `error`: Cloudflare could not set the hostname up (see `lastError`); it no longer holds a hostname slot.
    - `isPrimary` boolean, required: The primary host is the canonical address: the site's other connected hosts redirect (301) to it.
    - `lastError` string, nullable, required: Why setup failed or stalled, or null.
    - `createdAt` number, required: When the hostname was added, Unix ms.
    - `verifiedAt` number, required: When the hostname was last verified, Unix ms.
    - `connectSessionId` string, nullable, required: The connect session (`cs_…`) this host belongs to, or null (internal hosts, expired sessions).
    - `lastCheckedAt` number, required: When Cloudflare was last asked about this hostname, Unix ms.
    - `dnsRecords` DnsRecord[], required: The records to create. Empty for internal hosts. For a connected host they are all `found`; otherwise each carries a live `recordStatus`.
      - `type` string, required: DNS record type, e.g. `CNAME`, `TXT` or `A`.
      - `name` string, required: The record name to enter at the DNS provider (relative to the zone, e.g. `www` or `_acme-challenge.www`).
      - `value` string, required: The record value to enter.
      - `purpose` 'routing' | 'dcv' | 'apex', required: `routing`: points the hostname at Dotsy. `apex`: the same for a bare domain. `dcv`: proves you control the domain so the certificate can be issued.
      - `host` string: On `connect` records only: the session host this row belongs to.
      - `recordStatus` 'found' | 'missing' | 'mismatch' | 'waiting' | 'duplicate': A live DNS check of this record. Absent when no check has run. `duplicate` means extra TXT records at this name are blocking the certificate.
      - `nameError` 'doubled_suffix': The name looks like it was pasted as a full hostname and the provider appended the zone to it.
      - `useInstead` string: With `nameError`: the relative name to enter instead.
      - `dnsTxtCount` number: With `recordStatus: duplicate`: how many TXT answers exist at this name.
      - `dcvExpectedValues` string[]: Every certificate-check token that is currently acceptable at this name (Cloudflare rotated while pending).

## Other responses

- `201`: Added. For an external domain, `connect` says how to finish.
- `400`: The body is not valid (`invalid_body`: send `host` or `hosts`, at most two, apex+www only), or a host is not acceptable (`invalid_host`, `single_label_host`, `reserved_host`).
- `401`: Missing or invalid credentials.
- `403`: A paid plan is required for external domains (`plan_upgrade_required`), a hostname cap is reached (`custom_domain_limit`), or the caller cannot manage domains (owner only, no guest tokens, `guest_only` accounts).
- `404`: No such site (`site_not_found`).
- `409`: The host is already mapped to another site (`host_taken`).
- `502`: Cloudflare refused the custom hostname (`cf_error`). Retrying is safe.
- `503`: External domains are not configured on this server (`serving_unconfigured`).

---

[API](https://skmtc.dev/dotsy/apis/dotsy-api.md) · [All operations](https://skmtc.dev/dotsy/apis/dotsy-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dotsy/apis/dotsy-api/revisions/4272ed3b85d7?raw)
