---
title: Sites
description: Create, read, update, and delete sites, plus the complete site settings object — branding, social previews, search-engine verification — and its validation rules.
order: 2
---

# Sites

A site is one documentation property: one set of versions, one navigation tree, one asset namespace, one redirect table, and one or more hostnames.

All endpoints require an organization API key. `:site` accepts the site slug or its `site_...` id.

## List sites

```
GET /v1/sites
```

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

```json
{
  "sites": [
    {
      "id": "site_46269ed80042407a83242c9e94262b82",
      "slug": "acme",
      "name": "Acme Docs",
      "settings": {},
      "default_version_id": "ver_34c2a0bc33254ef6bfac1b128c1d36f6"
    }
  ]
}
```

Only your organization's sites are returned, ordered by slug.

## Create a site

```
POST /v1/sites
```

```bash
curl -s -X POST "$DOCSARY_API/v1/sites" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{"slug":"acme","name":"Acme Docs"}'
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `slug` | string | yes | Must match `^[a-z0-9][a-z0-9._-]*$`. Unique within your organization. |
| `name` | string | no | Display name. Defaults to the slug. |
| `settings` | object | no | See [Settings](#settings). Validated on write. |

```json
{ "id": "site_...", "slug": "acme", "subdomain": "acme.docsary.com" }
```

`201` on success.

| Response field | Meaning |
| --- | --- |
| `id` | The `site_...` id. Usable anywhere `:site` appears |
| `slug` | The slug you asked for |
| `subdomain` | The address allocated to this site, serving already — or `null` |
| `subdomain_note` | Present only when `subdomain` is `null`: why no address could be allocated |

**Read `subdomain` rather than assuming it.** The address is derived from your slug, but when that name is taken or reserved you are given `<your-org>-<your-slug>.docsary.com` instead:

```json
{ "id": "site_...", "slug": "api", "subdomain": "acme-api.docsary.com" }
```

If neither name is available the site is still created, with no address:

```json
{
  "id": "site_...",
  "slug": "guide",
  "subdomain": null,
  "subdomain_note": "no subdomain available: guide.docsary.com and acme-guide.docsary.com are taken or reserved. Attach your own hostname instead."
}
```

That is a note, not an error — the site is real and everything else about it works. Attach a hostname of your own, or create the site under a different slug. See [Domains](/domains).

| Response | Cause |
| --- | --- |
| `400 {"error":"invalid slug"}` | Malformed slug |
| `400` with a message naming the key | An unsafe settings URL or colour — see [Validation](#validation) |
| `403` with `quota: "sites"` | Your organization's [site ceiling](/access#the-ceilings) is already reached |
| `409 {"error":"site slug already exists ..."}` | That slug is already used in your organization |

A new site has no versions and no default version. Create a version before pushing pages — until then its address answers `404`.

## Read a site

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

```json
{
  "id": "site_...",
  "slug": "acme",
  "name": "Acme Docs",
  "settings": { "pwa": true },
  "default_version_id": "ver_...",
  "versions": [
    { "id": "ver_...", "slug": "1.0", "name": "1.0", "position": 0 },
    { "id": "ver_...", "slug": "0.9", "name": "0.9", "position": 1 }
  ],
  "domains": [
    { "hostname": "acme.docsary.com", "is_primary": 0, "kind": "saas", "verified_at": 1756300000000 },
    { "hostname": "docs.acme.com", "is_primary": 1, "kind": "custom", "verified_at": 1756300000000 }
  ]
}
```

This is the one call that shows a site's whole shape. Versions are ordered by position, then slug. A domain with a `verified_at` of `null` has not passed [verification](/api/domains#verify-a-domain) yet and is not serving.

`404 {"error":"site not found"}` if the slug or id does not resolve within your organization.

## Update a site

```
PATCH /v1/sites/:site
```

| Field | Type | Notes |
| --- | --- | --- |
| `name` | string | New display name |
| `settings` | object | **Replaces the settings object entirely.** Not a merge. |
| `default_version` | string | Version slug or id to make the default |

```bash
curl -s -X PATCH "$DOCSARY_API/v1/sites/acme" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{"default_version":"2.0"}'
```

```json
{ "ok": true }
```

Send only the fields you are changing. `404` if the site does not exist, or if `default_version` names a version that does not exist on it. An unsafe settings URL or colour is refused with `400` and nothing is written. The change is live on every page immediately.

:::warning
`settings` is a whole-object replace. Sending `{"settings":{"pwa":true}}` erases every other setting the site had. Read the site first, modify the object you get back, and send the complete result.
:::

## Delete a site

```
DELETE /v1/sites/:site
```

```json
{ "deleted": "acme", "assets_deleted": 12 }
```

This deletes the site and everything that hangs off it: every version, every page, every redirect, every domain attachment, and every asset — the stored files as well as their records, with `assets_deleted` reporting how many files went. Nothing is left behind to be billed or served. The site's `docsary.com` address goes with it and the name becomes available again, so recreating the site under the same slug is not guaranteed to give you the same address back.

It is immediate, and there is no undo. [Export](/api/pages#export) each version first if the content matters.

## Settings

The settings object controls presentation and per-site feature toggles. Every key is optional.

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `name` | string | the site's `name` | Display name in the header, browser title, PWA manifest, and `llms.txt` |
| `logo` | URL | none | Image in the header, favicon, and PWA icon |
| `colors.primary` | CSS colour | ink `#17181c` | Your brand colour. Takes over the header background, link colour, active nav item, focus ring, and quote/callout rules in both light and dark mode, plus the PWA theme colour and generated favicon |
| `colors.accent` | CSS colour | amber `#b0530a` light / `#eda94f` dark | The warning callout rule; also emitted as `--accent` for your own markup |
| `footer_links` | array of `{label, url}` | none | Links in the page footer |
| `posthog` | boolean | `true` | Pageview analytics |
| `pwa` | boolean | `false` | Offline mode — see [Offline and PWA](/offline) |
| `brand_stamp` | boolean | `true` | The "-by robota" credit in the footer |
| `og_image` | URL | generated | Social preview image for every page. Leave it unset and one is drawn for you per page — see [Social previews](/search-and-ai#social-previews) |
| `verification` | object of `{provider: token}` | none | Search-engine ownership tags — see [Verification](#verification) |

Example:

```json
{
  "name": "Acme Docs",
  "logo": "/assets/img/logo.svg",
  "colors": { "primary": "#0a7d55" },
  "footer_links": [
    { "label": "Privacy", "url": "https://acme.com/privacy" },
    { "label": "Contact", "url": "mailto:docs@acme.com" }
  ],
  "pwa": true,
  "posthog": false,
  "verification": { "google": "AbC123_dEf456" }
}
```

### Verification

Some search engines want proof that you control a site before they will show you its data. `verification` renders the meta tag each one documents, so verifying a site is a settings write rather than something that needs us to deploy anything.

| Provider key | Tag rendered |
| --- | --- |
| `google` | `google-site-verification` |
| `bing` | `msvalidate.01` |
| `yandex` | `yandex-verification` |
| `pinterest` | `p:domain_verify` |
| `facebook` | `facebook-domain-verification` |
| `ahrefs` | `ahrefs-site-verification` |
| `norton` | `norton-safeweb-site-verification` |

```bash
curl -s -X PATCH "$DOCSARY_API/v1/sites/acme"   -H "authorization: Bearer $DOCSARY_KEY"   -H "content-type: application/json"   -d '{"settings":{"verification":{"google":"AbC123_dEf456"}}}'
```

The tag appears on every page of the site immediately. Paste only the token, not the whole `<meta>` tag the provider shows you.

Only the providers above are accepted, and a token must be 4–256 characters of letters, digits and `_.:=+/-`. Both rules are refused with `400` rather than silently ignored — the tag name is chosen from the table above and never taken from your input, which is what keeps this from becoming a way to inject arbitrary markup into every page.

### Validation

Settings are checked on write. Anything that would be unsafe on the page is refused with `400`, the whole call is rejected, and nothing is stored — you never end up with a half-applied settings object.

**URLs** — `logo`, `og_image`, and each `footer_links[].url` must be an absolute `http:`, `https:`, or `mailto:` URL, or a site-relative path beginning with `/`, or a `#` fragment. Rejected: `javascript:`, `data:`, `vbscript:`, `file:`, protocol-relative `//host`, schemeless relative paths, and anything containing control characters.

```json
{ "error": "unsafe settings.logo URL (allowed: http:, https:, mailto:, or a /site-relative path)" }
```

**Colours** — `primary` and `accent` must be a hex value, an `rgb()`/`rgba()`/`hsl()`/`hsla()` function, or a plain CSS colour keyword. Anything else is refused:

```json
{ "error": "invalid settings.colors.primary (hex, rgb()/hsl(), or a CSS colour keyword)" }
```

Colours end up inside a stylesheet, so this is a security boundary rather than a formatting preference. `"#0a7d55"`, `"rgb(10 125 85)"`, and `"rebeccapurple"` are all fine; a value carrying a `}` or a `<` is not.

**Shape** — `settings` must be an object, `colors` must be an object, `footer_links` must be an array of objects. Each returns its own message.

The same URL and colour checks run again when the page is rendered. A value that somehow got past the check made when it was written is neutralized rather than emitted: an unsafe footer link renders as inert text with its label intact, and an unsafe logo falls back to the generated favicon.

### Analytics

`posthog` defaults to **on**. Enabled sites load a PostHog snippet configured with in-memory persistence, so no analytics cookie is written and no identifier survives the tab. Events are attributed by host, so several sites share one project without mixing.

Set `"posthog": false` to omit the snippet entirely. No script is loaded and no request is made.
