The v1 API

A site agent is the only thing a site exposes to the outside world. The admin talks to it entirely through this versioned, generic HTTP API — nothing about the admin ever touches a site’s filesystem or git repository directly.

Building a theme? You probably don’t need this page

Everything here is the admin’s own write API — tokens, drafts, publishing, batch edits, git history. It’s what the already-built admin app (or your own integration, if you’re building one) uses to manage content, not something a theme author touches directly. If you’re building a theme and just want to query your site’s own published content from the front end — a listing page, a search box, a filtered grid — see Querying your site instead, a single public endpoint that needs no token at all.

Authentication and scopes

Every write route requires a bearer token, configured per site in vhost/site.config.json. Tokens carry scopes, and a token only ever grants what it lists:

ScopeGrants
contentRead/write pages, drafts, menus, redirects and site settings; previews; history.
themeRead and push theme files — sections, blocks, layouts, snippets, assets, settings schema. Needed by npm run pull/push for the theme.
mediaUpload, list, and delete media.
Theme writes are the dangerous scope

Theme files are effectively code — a token that can write Liquid templates can do far more damage than one that can only write schema-validated JSON. Client-facing tokens should never be granted the theme scope. Tokens are rotatable per site from the admin’s registry, and rotating one requires a restart of the site process (tokens are loaded once at boot).

Search index

API
POST /v1/search/rebuild        rebuild the derived SQLite search index from disk

The agent keeps a derived, disposable SQLite index of published content — never authoritative, always rebuildable from the content files on disk. This route walks every published page and rebuilds it from scratch; call it after a publish if you need the index to reflect the latest content immediately.

Querying that index is a separate, public endpoint that needs no token at all — see Querying your site for the full read side (full-text search, structured filters, sort, pagination) and a worked front-end example.

Content and drafts

API
GET    /v1/content/:path                read a live content file (ETag included)
GET    /v1/drafts/:path                 read a draft file, same ETag behaviour
PUT    /v1/drafts/:path                 save a draft (If-Match; 409 if stale)
DELETE /v1/drafts/:path                 discard a draft
POST   /v1/publish                      publish one or more drafts in one commit
POST   /v1/publish-page/:path           set published: true on a page and commit
POST   /v1/unpublish/:path              set published: false on a page and commit
DELETE /v1/content/:path                delete a page, optional redirectTo
POST   /v1/content/move                 move a page + children, adding redirects
GET    /v1/content                      list content by type, path or draft status
GET    /v1/preview/:path                render a page with its draft applied
GET    /v1/preview-revision/:ref/:path  render a page as it was at a past revision

A move rewrites every internal link to the moved page (and to pages under it) in live pages, drafts, menus and site settings, in the same commit, and records a redirect from each old address. The page’s draft moves with it. GET /v1/preview/:path?settings=<JSON> renders with unsaved site settings in place of the saved ones, writing nothing — how the admin previews a settings change before it’s saved.

POST /v1/publish validates the draft again, copies it over the live file, removes the draft, and creates one commit with the authenticated editor as git author. There is no dedicated “create page” or “duplicate page” endpoint — both are just GET plus PUT from the admin side, using an existing page or theme template as the starting content.

API
GET /v1/links?to=/about   every page, draft, menu and setting linking to /about

Each reference names where the link is and how it’s written (/about#team). The admin shows this before a page is deleted. It’s read from the content each time, so it’s always current.

API
PUT    /v1/menus/:path              save a menu (items and name), live at once
POST   /v1/menus/rename             change a menu's handle (its filename)
GET    /v1/menus/references?handle= which theme templates use a menu handle
GET    /v1/redirects                list redirects
POST   /v1/redirects                add one
PUT    /v1/redirects                change one
DELETE /v1/redirects                remove one
GET    /v1/settings                 schema, saved values and what templates see
PUT    /v1/settings                 validate and save settings (needs If-Match)

Menus, redirects and site settings have no draft state: each save is one commit, live immediately.

Theme

API
GET  /v1/theme/files          every theme file with a hash of its contents
GET  /v1/theme/files/:path    one theme file
POST /v1/theme/push           commit new theme files, if unchanged since your pull
GET  /v1/theme/schemas        the schemas the admin builds its forms from
GET  /v1/theme/page-templates the page templates offered when creating a page

POST /v1/theme/push refuses a Liquid template that can’t be parsed, writes and commits everything at once, and switches the running site to the new theme with no restart. It needs the theme scope; this is what npm run push uses.

Optimistic concurrency

Every read returns an ETag. Every write requires If-Match with that ETag, and returns 409 Conflict if the file changed underneath you. This is the mechanism that catches two editors starting from different versions of the same page — the second writer’s save fails loudly instead of silently clobbering the first.

Batch writes

API
POST /v1/batch    an array of operations (draft writes, deletes, moves)
                  plus an optional publish instruction, one commit, all or nothing

Each write operation carries its own If-Match. This is the path for bulk edits — for example, changing a section setting across forty pages at once.

Git, as a feature

API
GET  /v1/git/log              commit history, filterable by path
GET  /v1/git/show/:ref/:path  read a file as it existed at a revision
POST /v1/git/revert           restore a path to a given revision as a NEW commit
POST /v1/git/commit           commit working-tree changes directly (escape hatch)
GET  /v1/deleted-pages        pages deleted in the last 90 days that aren't back

Reverting never rewrites history — it always creates a new commit that happens to restore old content. Draft checkpoint commits (an automatic background safety commit for open drafts) are flagged so the admin can hide them from history views by default.

GET /v1/deleted-pages (scope content) lists pages deleted in the last 90 days, newest first, read from git’s own record of deletions: each with its path, url, name, title, deletedAt, deletedBy, and the ref to restore it from with POST /v1/git/revert. A moved page isn’t a deletion, and a page that exists again (live or as a draft) isn’t listed. name was added in 0.9.1.

Media

API
GET    /v1/media           list uploaded files
POST   /v1/media           upload (multipart), named from a hash of the bytes
DELETE /v1/media/:path     permanent - no soft delete, no history

Uploads are content-addressed: the filename is derived from a hash of the file’s own bytes, so an identical re-upload dedupes for free and a collision on distinct content is not a real concern. Images (JPG, PNG, GIF, WebP) and video (MP4, WebM) are accepted; SVG is rejected outright, not sanitised. Media is served with HTTP range support, which Safari and iOS need before they will play a video. Upload size is capped at the multipart-stream layer itself, so an oversized body is rejected mid-stream rather than fully buffered into memory first.

Capabilities

API
GET /v1/capabilities

Returns the agent package version, the content schema version, the active SQLite driver, the configured max media upload size, and a manifest of optional features. The admin calls this to decide what to show or hide for a given site, and it doubles as a real, always-available health check — suitable for a platform’s own liveness probe.

Security posture

  • Path sanitisation is the number one concern. Every :path parameter is resolved and confirmed to sit inside the content or drafts root before any filesystem operation.
  • Commit authorship is passed through from the authenticated editor on every write, and set as the git author on resulting commits.
  • Rate limiting applies to every write endpoint, plus the two read endpoints reachable with no token at all (GET /v1/capabilities, GET /search.json) — generous defense-in-depth there, not a throttle on legitimate use.
  • IP allowlisting is available per site but optional — scoped tokens, path sanitisation, and schema validation are the primary controls, not network position.

Next

Continue to Hosting & deployment to run a site somewhere real, including the full site.config.json reference.