---
title: Quickstart
description: Go from an API key to a live docs site — create a site, create a version, push a markdown folder, verify what was served.
order: 1
---

# 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](/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:

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

## 1. Check the key

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

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

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

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

```bash
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"}'
```

```json
{ "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](/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:

```bash
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:

```bash
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"
    }
  }'
```

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

```bash
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:

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

Read one page back as rendered HTML:

```bash
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:

```bash
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:

```bash
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:

```bash
curl -s -X POST "$DOCSARY_API/v1/sites/acme/domains/docs.acme.com/verify" \
  -H "authorization: Bearer $DOCSARY_KEY"
```

```json
{ "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](/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

- `https://acme.docsary.com/` and `/quickstart` rendered with navigation, table of contents, search, and dark mode.
- `/quickstart.md` and `/index.md` serving raw markdown.
- `/llms.txt`, `/llms-full.txt`, `/sitemap.xml`, `/robots.txt`.
- `/search?q=install` over an index built during the push.

Next: [Writing docs](/writing).
