---
title: Domains
description: List, attach, verify, and detach the hostnames a site serves on — the automatic docsary.com address, and custom domains with their TXT challenge and CNAME.
order: 7
---

# Domains

Every site is allocated a `docsary.com` address when it is created, and that address is already serving. This endpoint is how you inspect it, and how you add a hostname of your own on top.

For the flow end to end — including primary versus staging hosts — see [Domains](/domains).

All endpoints require an organization API key. `:site` accepts a slug or an id.

## Kinds of domain

| `kind` | What it is | Who gets one |
| --- | --- | --- |
| `saas` | The `<name>.docsary.com` address allocated when the site was created. Verified and serving from the moment it exists. | Every plan, automatically |
| `custom` | A hostname you own. Needs a `TXT` ownership challenge and a `CNAME`. | Every plan, up to the custom-domain ceiling |
| `route` | A hostname set up on your behalf. Attaches already verified. | Provisioned accounts only |

## List domains

```
GET /v1/sites/:site/domains
```

```bash
curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/sites/acme/domains"
```

```json
{
  "domains": [
    {
      "hostname": "acme.docsary.com",
      "is_primary": 0,
      "kind": "saas",
      "verified": true,
      "verified_at": 1756300000000
    },
    {
      "hostname": "docs.acme.com",
      "is_primary": 1,
      "kind": "custom",
      "verified": true,
      "verified_at": 1756300000000,
      "registered": true,
      "traffic": {
        "type": "CNAME",
        "name": "docs.acme.com",
        "value": "ssl.docsary.com",
        "how": "Point docs.acme.com at ssl.docsary.com with a CNAME once ownership is verified. The certificate is issued and renewed for you; there is nothing to install."
      }
    },
    {
      "hostname": "staging.acme.com",
      "is_primary": 0,
      "kind": "custom",
      "verified": false,
      "verified_at": null,
      "registered": false,
      "verification": {
        "type": "TXT",
        "name": "_docsary.staging.acme.com",
        "value": "docsary-verify=8f1c...",
        "how": "Publish this TXT record, then POST to /v1/sites/<site>/domains/staging.acme.com/verify. The hostname is not served until it verifies."
      },
      "traffic": {
        "type": "CNAME",
        "name": "staging.acme.com",
        "value": "ssl.docsary.com",
        "how": "..."
      }
    }
  ]
}
```

| Field | Meaning |
| --- | --- |
| `kind` | `saas`, `custom`, or `route` — see the table above |
| `is_primary` | `1` on the site's canonical host, `0` otherwise |
| `verified` | Whether the hostname is serving. Always `true` for `saas` |
| `verified_at` | Millisecond timestamp of verification, or `null` |
| `verification` | Present only while a `custom` hostname is unverified: the record to publish |
| `traffic` | Present on every `custom` hostname, verified or not: the `CNAME` to publish |
| `registered` | `custom` only. `true` once a certificate has been requested for the hostname |

This call is how you recover both records if you lost the attach response.

## Attach a domain

```
POST /v1/sites/:site/domains
```

```bash
curl -s -X POST "$DOCSARY_API/v1/sites/acme/domains" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{"hostname":"docs.acme.com","primary":true}'
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `hostname` | string | yes | Lowercased and trimmed. Must be a dotted hostname with a suffix of two or more letters. May not end in `docsary.com`. |
| `primary` | boolean | no | Marks this the site's canonical host and clears the flag from its other domains. |

```json
{
  "hostname": "docs.acme.com",
  "kind": "custom",
  "verified": false,
  "verification": {
    "type": "TXT",
    "name": "_docsary.docs.acme.com",
    "value": "docsary-verify=8f1c2d0a4b6e8f1c2d0a4b6e8f1c2d0a",
    "how": "Publish this TXT record, then POST to /v1/sites/<site>/domains/docs.acme.com/verify. The hostname is not served until it verifies."
  },
  "traffic": {
    "type": "CNAME",
    "name": "docs.acme.com",
    "value": "ssl.docsary.com",
    "how": "Point docs.acme.com at ssl.docsary.com with a CNAME once ownership is verified. The certificate is issued and renewed for you; there is nothing to install."
  }
}
```

`201` on success. The attachment exists and is unverified; nothing is served on it, and no certificate is requested, until it verifies.

The `verification` token is generated per attachment. Detaching and re-attaching a hostname issues a new one, so publish the record from the response you are working with rather than one you saved earlier. The `traffic` record never changes.

### Errors

| Response | Cause |
| --- | --- |
| `400 {"error":"invalid hostname"}` | Missing, malformed, or dotless hostname |
| `400` containing `is not attachable` | The hostname ends in `docsary.com`. Those are allocated at site creation, not attached |
| `403` with `"quota":"custom_domains"` | Your organization's custom-domain ceiling is already reached |
| `409 {"error":"hostname already attached","to_this_site":false}` | The hostname is attached to another site |
| `409 {"error":"hostname already attached","to_this_site":true}` | The hostname is already attached to this site |
| `404 {"error":"site not found"}` | The site slug or id does not resolve |

There is no plan gate on this endpoint. Attaching a hostname you own works on every plan; the only bound is a count:

```json
{
  "error": "quota exceeded: custom_domains limit is 3 on plan 'free'",
  "quota": "custom_domains",
  "limit": 3,
  "plan": "free"
}
```

That ceiling exists because each attached hostname gets a certificate issued and renewed for it, which is real work done on your behalf — not because a larger number is for sale. It is counted across your whole organization rather than per site, and your `docsary.com` addresses never count against it. If three is not enough for a real project, [ask](/access#the-ceilings).

Hostnames are unique across every site. `to_this_site` distinguishes a re-run from a genuine conflict.

Some accounts are provisioned so that a hostname is set up for you rather than challenged. Those attachments come back `"kind": "route"` and `"verified": true`, alongside a short setup report — one entry per step, each reading `created`, `exists`, `failed`, or `skipped` with a reason. Nothing is required of you when that report appears; it is there to be read, not acted on.

## Verify a domain

```
POST /v1/sites/:site/domains/:hostname/verify
```

Publish the `TXT` record from the attach response, then call this. It resolves the record live on every call.

```bash
curl -s -X POST "$DOCSARY_API/v1/sites/acme/domains/docs.acme.com/verify" \
  -H "authorization: Bearer $DOCSARY_KEY"
```

```json
{
  "hostname": "docs.acme.com",
  "verified": true,
  "traffic": {
    "type": "CNAME",
    "name": "docs.acme.com",
    "value": "ssl.docsary.com",
    "how": "..."
  },
  "certificate": { "status": "created" }
}
```

The hostname begins serving your site once this returns `verified: true`, and the `traffic` block is repeated here so the next thing to do is in front of you.

| Response | Meaning |
| --- | --- |
| `200 {"verified":true}` | Verified. The hostname serves as soon as its `CNAME` is live. |
| `200 {"verified":true,"already_verified":true}` | It was already verified. If no certificate had been requested yet, this call retries that. |
| `409` with `expected` and `found` | The record was not found. `found` lists what did resolve at that name. |
| `409 {"error":"domain has no verification token; re-attach it"}` | The attachment predates its challenge — detach and attach again. |
| `502 {"error":"DNS lookup failed: ..."}` | The lookup itself failed. Retry. |
| `404 {"error":"domain not found"}` | That hostname is not attached to this site. |

A `409` is the ordinary answer to "the record has not propagated yet". Compare `expected` against `found`, wait out your provider's TTL, and call again — there is no cached failure to clear.

### The `certificate` block

| `status` | Meaning |
| --- | --- |
| `created` | A certificate was requested for the hostname. It issues once the `CNAME` resolves to us. |
| `exists` | One had already been requested. Nothing to do. |
| `skipped` | That step could not run right now, with a `reason`. Call verify again later. |
| `failed` | The request was refused, with an `error`. Call verify again; if it persists, send us the text. |

**A `skipped` or `failed` certificate does not undo verification.** Ownership is proven, the attachment stands, `verified` is still `true`, and re-running verify retries only the certificate. This block is a progress report, not a failure mode.

## Detach a domain

```
DELETE /v1/sites/:site/domains/:hostname
```

```bash
curl -s -X DELETE "$DOCSARY_API/v1/sites/acme/domains/docs.acme.com" \
  -H "authorization: Bearer $DOCSARY_KEY"
```

```json
{ "deleted": "docs.acme.com", "certificate": { "status": "deleted" } }
```

The hostname stops serving and its certificate is released. `certificate` reads `deleted`, `absent` (there was none to release), `skipped`, or `failed` with a reason.

**Your DNS records are never touched** — they are yours, and removing them is your call.

`404 {"error":"domain not found"}` if the hostname is not attached to this site.

Detaching your `docsary.com` address works the same way and frees that name for somebody else, so do it deliberately. Deleting the site detaches everything it had.

## When changes take effect

Attaching, verifying, detaching, or changing the primary flag takes effect within about half a minute, with no deploy and nothing to purge. A brand-new custom hostname additionally waits on its certificate, usually a minute or two after its `CNAME` goes live.

A hostname that is unattached, or attached but unverified, gets a plain-text `404`: `docsary: no site bound to this host`.
