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