On this page
Conventions and errors
Base URL
The API lives at:
https://api.docsary.com/v1
On a docs hostname, /v1 is not the API: that path belongs to the docs site, which is why v1 is a reserved page path.
Authentication
Every endpoint except /__health and the endpoint index takes a bearer token:
Authorization: Bearer dsy_XXXX
Organization API keys start with dsy_ followed by 64 hexadecimal characters. A key sees only its own organization's sites — the organization is taken from the token, never from the request, so there is no cross-tenant path to guard.
Keys are provisioned for you; see Access.
Scopes. A key is write-capable or read-only. A read-only key may issue GET requests and nothing else; a mutation with one returns:
{ "error": "this API key is read-only (scopes: 'read'); a write-capable scope is required" }
with 403. Ask for a read-only key for anything that only needs to look — a status check, a docs linter in CI, a dashboard.
Handling. Keys are stored only as a hash. A key is displayed once, when it is issued, and cannot be recovered afterwards. Treat one as deploy credentials: it can rewrite every page on every site in your organization. Ask for a separate named key per consumer so revoking one does not take down the others, and keep keys out of your repository. GET /v1/whoami reports which key you are holding, its scopes, and your plan. Revocation is immediate — a revoked key returns 401 on its next request.
Identifiers
Every record has a prefixed identifier that says what it is:
| Prefix | Record |
|---|---|
org_ |
organization |
site_ |
site |
ver_ |
version |
pg_ |
page |
ast_ |
asset |
dom_ |
domain |
rd_ |
redirect |
rep_ |
report |
Where a path takes :site or :ver, either the slug or the id is accepted. /v1/sites/acme/versions/1.0 and /v1/sites/site_46269.../versions/ver_34c2a... address the same thing. Slugs are readable and stable in practice; ids are stable by construction and survive a rename.
Pages are addressed by path, not by id.
Requests
Bodies are JSON with content-type: application/json, with one exception: asset uploads send the file as a raw body and use the content-type header to declare the file's own type.
PATCH bodies are sparse — send only the fields you are changing. One exception matters: settings on a site is a whole-object replace, not a merge. Read the current object, modify it, and send it back complete.
Unknown fields in a request body are ignored.
Responses
Success responses are JSON objects. There is no envelope: a list endpoint returns {"sites": [...]}, not {"data": {...}}.
Errors are a single-key object:
{ "error": "site not found" }
Some errors carry extra fields alongside error: attaching an already-attached hostname adds to_this_site, and a limit refusal adds quota, limit, and plan.
Status codes
| Code | Meaning |
|---|---|
200 |
Success |
201 |
Created — a new site, version, page, asset, or domain |
400 |
Malformed request: invalid slug, invalid path, unsafe URL or colour, unknown query parameter value, malformed percent-encoding, missing required field |
401 |
Missing or invalid bearer token |
403 |
Read-only key on a write, or a ceiling reached |
404 |
No such site, version, page, asset, or domain |
409 |
Conflict: slug already exists, version slug would shadow existing page URLs, hostname already attached, domain not yet verified |
413 |
Asset over the 20 MB per-file cap, over your organization's storage ceiling, or an import request over the per-request cap |
429 |
A rate limit: writes per minute, failed authentications per minute, or the hourly report filing limit. Carries retry-after |
502 |
An external lookup failed — currently only domain verification |
503 |
Asset storage is temporarily unavailable |
A 404 on a nested route can mean the parent is missing. /v1/sites/acme/versions/1.0/pages returns {"error":"site not found"} when the site is wrong and {"error":"version not found"} when the site is right and the version is not. The message distinguishes them.
Limits
Docsary is free, and every organization runs under a set of ceilings on how many sites it may run, how many versions each site may hold, how many pages a version may hold, how many redirects a site may carry, how many custom domains it may attach, and how much asset storage it may use. They exist so one runaway script cannot exhaust shared capacity — there is no larger tier on the other side of them. The numbers are in Access. The docsary.com address every site is given at creation is not one of them: it arrives with the site and counts against nothing.
A call that would cross a ceiling is refused rather than partially applied. The response body names it precisely: error states it in words, quota says which limit was hit — sites, versions_per_site, pages_per_version, redirects_per_site, custom_domains, or asset_bytes — limit gives the number that applied, and plan names your plan. Nothing has to be inferred from a bare 403.
Most refusals return 403; the storage one returns 413. A bulk import that runs out of room mid-chunk keeps and reports the pages it did write, in a diff field alongside the error, so you can see exactly where it stopped. GET /v1/whoami reports which plan you are on.
No capability is gated on the plan. Every endpoint in this reference works on every organization; only the counts differ.
Rate limits
Three limits are about rate rather than count. Each answers 429 with a retry-after header in seconds, and a body carrying limit, window, and retry_after:
{
"error": "too many write requests: this organization may make 240 writes per minute. Slow the push down and retry; nothing was written.",
"limit": 240,
"window": "minute",
"retry_after": 43
}
| What | Ceiling | Counted per |
|---|---|---|
Write requests — anything that is not GET, HEAD or OPTIONS |
240 per minute | organization |
| Failed authentications | 20 per minute | calling address |
| Reports filed | 20 per hour | organization |
Reads are never rate limited. A GET costs one lookup and is cacheable, so there is no ceiling on reading your own content however often you do it.
A 429 means nothing was written. The refusal happens before the request reaches the handler, so retrying after retry_after seconds is safe and is never a duplicate.
The write ceiling is set well above a real push: an import runs at roughly six files per request, so a two-thousand-page manual is a few hundred requests and finishes inside two windows without pausing. If you are hitting it, the cause is usually a retry loop rather than a large site.
The authentication ceiling counts failures, not requests: a working key is never throttled by it, however hard you push. Twenty rejected tokens in a minute from one address earns a 429 until the minute is out. Two things follow from that. Retrying a 401 in a tight loop makes things worse rather than better — fix the key. And because the count is per calling address, a large office behind one address can be locked out by one colleague's broken script for the remainder of the minute; the window is short by design for exactly that reason.
Request size
Send about six files per import request. Keep chunks small. Chunking is safe: import is additive, so any number of chunks may be posted before a final prune call establishes the complete page set.
One import request may carry at most 100 files and 2 MB of markdown. Over either, the call is refused with 413 and nothing is written.
Idempotency
Page writes are content-addressed. Each page is keyed by a hash of its markdown source; an import whose hashes match what is stored reports those paths as unchanged and writes nothing.
Pushing the same folder twice is therefore free, and pushing a folder in which one file changed costs one page's work. Retrying a failed or partially-applied push is safe.
Publishing
Content is live as soon as the write returns. There is no build step, no publish gate, no purge call, and no propagation delay to wait out.
Served pages carry two publish-confirmation headers: x-docsary-epoch, whose value changes whenever the site is written to, and x-docsary-cache, reading hit or miss. Together they let a deployment script confirm that a push actually reached the reader-facing site.