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.
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:
| Scope | Grants |
|---|---|
content | Read/write pages, drafts, menus, redirects and site settings; previews; history. |
theme | Read and push theme files — sections, blocks, layouts, snippets, assets, settings schema. Needed by npm run pull/push for the theme. |
media | Upload, list, and delete media. |
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
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
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.
Links
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.
Menus, redirects and site settings
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
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
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
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
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
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
:pathparameter 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.