Docsary
Markdown Ask Claude Ask ChatGPT
On this page

Quickstart

This walks from an API key to a served site. It takes four API calls.

Before you start

You need an organization API key. Keys start with dsy_ and are provisioned for you; see Access. Keep it out of source control.

The API base URL is:

https://api.docsary.com/v1

Every request authenticates with a bearer token:

Authorization: Bearer dsy_XXXX

Set it up in your shell:

export DOCSARY_KEY=dsy_XXXX
export DOCSARY_API=https://api.docsary.com

1. Check the key

curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/whoami"
{
  "kind": "org",
  "org": { "id": "org_...", "slug": "acme", "name": "Acme", "plan": "free" },
  "scopes": "write"
}

A 401 {"error":"invalid token"} means the key is wrong or revoked. A 401 {"error":"missing bearer token"} means the header did not arrive.

plan is the set of limits your organization runs under, and scopes says whether this key may write or only read.

2. Create a site

A site is one docs property: one address, one navigation tree, one set of versions.

curl -s -X POST "$DOCSARY_API/v1/sites" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{"slug":"acme","name":"Acme Docs"}'
{ "id": "site_...", "slug": "acme", "subdomain": "acme.docsary.com" }

201 Created. Note the subdomain: your site already has a live address, allocated from the slug you just chose. Keep it — step 6 is opening it.

The slug must match ^[a-z0-9][a-z0-9._-]*$ and is unique within your org. A duplicate returns 409. Everywhere :site appears in the API you may pass either the slug or the site_... id.

3. Create a version

Pages belong to a version, never directly to a site. Create one before pushing.

curl -s -X POST "$DOCSARY_API/v1/sites/acme/versions" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{"slug":"1.0","name":"1.0"}'
{ "id": "ver_...", "slug": "1.0" }

The first version created on a site automatically becomes that site's default version, which means it serves from unprefixed URLs. See Versions.

4. Push a markdown folder

Lay out a folder. File paths become URL paths:

docs/
  index.md              →  /
  quickstart.md         →  /quickstart
  guide/index.md        →  /guide
  guide/install.md      →  /guide/install

With the push script

The push script walks a directory, chunks it, and posts it:

node scripts/push.mjs ./docs --site acme --version 1.0 --prune
pushed 24/24
done: 24 created, 0 updated, 0 unchanged, 0 pruned

It reads DOCSARY_KEY from the environment, or falls back to ~/.secrets/docsary-api-key, and reads DOCSARY_API for the base URL. --prune deletes any page in the version that is not in the folder you just pushed; omit it to add and update without deleting. The script exits non-zero if any file was rejected.

With a raw manifest POST

The push script is a convenience wrapper over one endpoint. The manifest is a JSON object mapping file paths to markdown source:

curl -s -X POST "$DOCSARY_API/v1/sites/acme/versions/1.0/import" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{
    "files": {
      "index.md": "---\ntitle: Home\norder: 0\n---\n\n# Acme Docs\n\nWelcome.\n",
      "quickstart.md": "---\ntitle: Quickstart\norder: 1\n---\n\n# Quickstart\n\nInstall it.\n"
    }
  }'
{
  "created": ["", "quickstart"],
  "updated": [],
  "unchanged": [],
  "deleted": [],
  "rejected": []
}

Paths in the response are normalized page paths, not filenames — index.md became the empty path, which is the version root.

Send roughly six files per request — keep chunks small. The push script uses six. Chunking is safe because import is additive: post as many chunks as you need, then send one final prune call listing every path that should survive.

curl -s -X POST "$DOCSARY_API/v1/sites/acme/versions/1.0/import" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{"prune":{"keep":["index.md","quickstart.md"]}}'

Re-pushing identical content is free. Each page is keyed by a hash of its markdown; a matching hash returns unchanged and nothing is written.

5. Verify

List what landed, with the generated navigation tree:

curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/sites/acme/versions/1.0/pages"

Read one page back as rendered HTML:

curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/sites/acme/versions/1.0/pages/quickstart?format=html"

Round-trip the whole version back to markdown:

curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/sites/acme/versions/1.0/export"

6. Open it

It is already live. Go to the subdomain from step 2:

https://acme.docsary.com/

That address was allocated when you created the site and serves over HTTPS. There is no sixth API call to make.

Your own domain, optionally

You can put the same site on a hostname you own. Attach it:

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}'

The response hands you two DNS records. Publish the TXT one to prove the hostname is yours, then confirm it:

curl -s -X POST "$DOCSARY_API/v1/sites/acme/domains/docs.acme.com/verify" \
  -H "authorization: Bearer $DOCSARY_KEY"
{ "hostname": "docs.acme.com", "verified": true, "certificate": { "status": "created" } }

Then publish the second record and the cutover is done:

docs.acme.com    CNAME    ssl.docsary.com

The certificate is issued and renewed for you; there is nothing to install. Your docsary.com address keeps working as a staging URL. See Domains for the whole flow, primary versus staging hosts, and what to do when verification does not pass on the first try.

What you have now

Next: Writing docs.