Docsary
Markdown Ask Claude Ask ChatGPT
On this page

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
curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/sites"
{
  "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
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. Validated on write.
{ "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:

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

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

{
  "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.

Response Cause
400 {"error":"invalid slug"} Malformed slug
400 with a message naming the key An unsafe settings URL or colour — see Validation
403 with quota: "sites" Your organization's site ceiling 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
{
  "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 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
curl -s -X PATCH "$DOCSARY_API/v1/sites/acme" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{"default_version":"2.0"}'
{ "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.

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
{ "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 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
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
verification object of {provider: token} none Search-engine ownership tags — see Verification

Example:

{
  "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
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.

URLslogo, 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.

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

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

{ "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.

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