HTTP API reference
All endpoints sit under /api/. Public visitor traffic does NOT go through this server — visitors hit the static artifacts directly via S3+CloudFront, the manifest-pointer L@E resolver, or a customer-owned GitHub repo (depending on STATICOWL_DEPLOY_TARGET). The /api/* surface is for the admin UI, programmatic clients, MCP servers, and SDK consumers.
Authentication
The CMS uses Cognito ID tokens in an Authorization: Bearer … header. The browser admin (/admin/) uses amazon-cognito-identity-js to obtain and refresh tokens. Programmatic clients obtain a Cognito token via SRP flow or use a CMS-minted API key (prefix gcms_…).
Most endpoints additionally require an X-Site-Id header identifying which site the request applies to. /api/auth/*, /api/health, /api/access/*, /api/sites/* (top-level) and /api/leads are the exceptions.
Auth order (request lifecycle)
request → cognitoAuthMiddleware → req.auth.{userId, email, name}
→ platformContext → req.platformUser, req.platformRole
→ requireSiteContext → req.services.{graph, content, ...}
→ route handler → service call → engine
See architecture.md for the full picture.
Conventions
- Requests and responses are JSON unless otherwise noted.
- Dates are ISO 8601.
- Errors return
{ "error": "…" }with a non-2xx status. - List endpoints accept
?q=(search), some accept?status=,?page=,?limit=. - Pagination: list responses are bare arrays for now; pagination cursors will be added per-route as scale demands.
Status codes
| Code | When |
|---|---|
200 |
Success (GET / PATCH / PUT) |
201 |
Resource created (POST) |
204 |
Success, no body (DELETE) |
400 |
Bad input (validation, malformed JSON, business rule violation) |
401 |
Missing or invalid auth |
403 |
Authenticated but no access to the requested site/resource |
404 |
Resource not found |
409 |
Conflict (deploy gate failure, version conflict) |
429 |
Rate limit exceeded |
500 |
Internal server error |
503 |
Memory pressure / overload — request shed |
504 |
Query timeout (default 120s) |
Health
GET /api/health
No auth. Returns { status: "ok", lastBuildAt, lastBuildId }.
Access / sites
GET /api/access/my-sites
Returns the sites the current user has access to — used by the admin UI to bootstrap the site picker. No X-Site-Id needed.
[
{ "siteId": "...", "siteName": "Kindatechnical", "graphName": "kindatechnical", "role": "admin" }
]
GET /api/access/site-users (admin only, site-scoped)
Lists users with access to the current site.
POST /api/access/grant (admin only)
Grants a user access to the current site. Body: { userId, role } (role ∈ admin | editor | publisher).
GET /api/sites/
Owner / admin: lists every site on the platform. Other roles: lists only sites the user has access to.
POST /api/sites/
Creates a new site. Grants the creator admin access automatically. Body:
{
"name": "My Site",
"publicUrl": "https://example.com",
"slug": "my-site",
"starterKit": "developer | author | designer | none",
"description": "...",
"mantra": "...",
"theme": { "primary": "#5A7D4D", "accent": "#FBBF24" }
}
If starterKit is omitted, the user's persona is used. Starter kits seed the new site's graph with default content types + templates.
GET /api/sites/:id (admin / owner / member)
Site detail.
DELETE /api/sites/:id (owner only)
Soft-deletes the site. Engine retains data for the configured retention period.
Auth (site-member management)
These are NOT login/registration — Cognito handles that at the gateway. These manage site members for the current site.
GET /api/auth/users
Lists members of the current site. Requires X-Site-Id.
PUT /api/auth/users/:id/role
Change a member's site role. Body: { role: "admin" | "editor" | "publisher" | "viewer" }.
DELETE /api/auth/users/:id
Revokes the user's access to the current site.
GET /api/auth/api-keys
Lists CMS-minted API keys for the current site (prefix gcms_…). Returns key metadata, not the secret.
POST /api/auth/api-keys
Mints a new key. Body: { name, role }. Response includes the secret once — store it.
DELETE /api/auth/api-keys/:id
Revokes a key.
Content
Content-type names are used as path segments (e.g., article, page).
| Method | Path | Description |
|---|---|---|
GET |
/content/:type |
List items. ?q, ?status, ?limit. |
POST |
/content/:type |
Create item. Body: { fields, relationships? }. |
GET |
/content/:type/:id |
Get item by full id (type:slug). |
PATCH |
/content/:type/:id |
Partial update. |
DELETE |
/content/:type/:id |
Delete item. |
POST |
/content/:type/:id/blocks |
Append a block to the item's body. Body: { blockType, fields, references?, order? }. |
DELETE |
/content/:type/:id/blocks/:bid |
Remove a block. |
POST |
/content/:type/:id/publish |
Transition to published (shortcut for workflow transition). |
Content body persists the Editor.js output format: { time, blocks: [{ type, data }], version }.
Content types
| Method | Path | Description |
|---|---|---|
GET |
/types |
List content types for the site. |
GET |
/types/:name |
Get a type definition. |
POST |
/types |
Create a type. Body: { name, label, fields, relationships? }. |
PATCH |
/types/:name |
Update a type. |
Templates, scripts, queries, routes, views, block-types, workflows, locales, transforms, widgets
Each of these is a CRUD-style schema/asset endpoint:
| Resource | Path |
|---|---|
| Templates (rendering) | /templates, /templates/:name |
| Scripts (server-side hooks) | /scripts, /scripts/:name |
| Queries (saved Cypher) | /queries, /queries/:name, POST /queries/:name/run |
| Routes (URL → template/content) | /routes, /routes/:slug |
| Views (admin views) | /views, /views/:name, /views/:name/seo |
| Block types | /block-types, /block-types/:name |
| Workflows | /workflows, /workflows/:vvId/state, POST /workflows/:vvId/transition |
| Locales | /locales, /locales/:code, /locales/:code/translations |
| Transforms (lifecycle hooks) | /transforms, /transforms/:id |
| Widgets | /widgets, /widgets/:name |
All follow the same shape: GET / lists, GET /:name gets, POST / creates, PUT|PATCH /:name updates. See Lifecycle hooks for transforms specifically.
Releases / Deployments
The /api/releases namespace is where the deploy story lives. Releases are immutable bundles; Deployments point a Release at an environment with a validFrom. See features.md → Releases for concepts.
Environments
| Method | Path | Description |
|---|---|---|
GET |
/releases/environments |
List environments for the site. |
PUT |
/releases/environments/:id |
Upsert an environment (id, name, order, autoBuild, requiresApproval, publicUrlOverride). |
Releases
| Method | Path | Description |
|---|---|---|
GET |
/releases?state=draft|approved|deployed&limit=50 |
List releases. |
POST |
/releases |
Create a Release. Body: { name, description, intent, derivedFromReleaseId, includes }. |
GET |
/releases/:id |
Get release + its INCLUDES. |
POST |
/releases/:id/includes |
Add a Versionable to a draft release. Body: { kind, versionableId, version, hash, at }. |
POST |
/releases/:id/transition |
State transition. Body: { state }. Allowed transitions per release-service.ts. |
Deployments
| Method | Path | Description |
|---|---|---|
GET |
/releases/deployments?envId=&limit=50 |
List recent deployments. |
POST |
/releases/:id/deploy |
Create a Deployment. Body: { environmentId, intent, validFrom, skipReviewGate? }. Returns 409 if blocking Review findings exist or required reviews are stale. |
POST |
/releases/deployments/:id/rollback |
Roll back. Body: { rollbackToReleaseId }. Creates a new Deployment fact pointing at the older Release; the rolled-back-from Deployment is marked superseded. |
Drip publishing
Schedule N approved Releases across a time window. Each Release gets its own Deployment with a staggered validFrom; the scheduler fires each Deployment at its time. Three patterns: even, random, jittered. Preview-then-approve.
| Method | Path | Description |
|---|---|---|
POST |
/releases/drip-preview |
Compute the timestamps without writing anything. Body: { count|releaseIds, windowStart, windowEnd, pattern, maxJitterMs?, minSpacingMs?, seed? }. Returns { schedule, pairs? }. |
POST |
/releases/drip-schedule |
Create the Deployments. Body: same as preview, plus environmentId. Validates every Release is approved or deployed first — atomic. Returns { schedule, deployments: [...] }. |
GET |
/releases/queue?envId=&limit=100 |
Future-dated pending Deployments, ordered by validFrom ascending. |
See features.md → Drip publishing for examples.
Replay
| Method | Path | Description |
|---|---|---|
GET |
/releases/replay?envId=&atTime= |
Returns the Deployment + Release + includes that were live in envId at atTime (defaults to now). The basis for "show me the site as it was on Jan 15." |
Build
| Method | Path | Description |
|---|---|---|
POST |
/build |
Trigger a build. Body: { env: "dev" }. Returns build status + deploy result. |
GET |
/build/last?env= |
Last build for an environment. |
GET |
/build/history?env=&limit=50 |
Recent builds. |
POST |
/build/rollback/:buildId |
Rollback to a prior build (legacy mode; new mode uses Release rollback). |
AI
The /api/ai namespace exposes the AI assistant capabilities. See ai.md for the full feature breakdown.
Common shape:
POST /api/ai/chat— chat completion proxied through Anthropic / OpenAIPOST /api/ai/draft— voice-aware content draftPOST /api/ai/derive— multi-asset derive (one upload → blog + tweets + LinkedIn + email + alt text)POST /api/ai/import— document → structured pagesPOST /api/ai/visual-edit— edit-this-paragraphPOST /api/ai/bulk-rewrite— site-wide diff-preview rewritePOST /api/ai/site-health— full-site audit + auto-fix proposalsPOST /api/ai/voice-profile— train / preview voice profile
Media
GET /api/media/stock?provider=pexels|unsplash|pixabay&q=&orientation=landscape
Proxies to the named stock search provider. Returns a normalized { items: [{ id, url, thumb, photographer, source, width, height }] }.
GET /api/media
List uploaded assets for the current site, ordered by upload time (newest first). Query params: ?q=filename-substring, ?countOnly=1 (returns { total } only).
POST /api/media/upload
Multipart form with field file. Accepts images, videos, PDFs. 25 MB max. Returns:
{
"ok": true, "id": "asset:...", "url": "/media/assets/<sha>.png",
"sha256": "...", "filename": "...", "mimetype": "image/png",
"bytes": 12345, "width": 1024, "height": 768,
"uploadedBy": "user:...", "uploadedAt": "..."
}
Storage: local filesystem (MEDIA_DIR, default /opt/graphiquity/data/cms-media). File's SHA is both the filename and the dedup key.
DELETE /api/media/:id
Removes the Asset node and unlinks the file.
GET /media/assets/<sha>.<ext>
Static file serving. Not under /api/; no auth on reads.
Page kits
Reusable layout fragments (hero sections, CTAs, image galleries, testimonials).
| Method | Path |
|---|---|
GET |
/page-kits |
GET |
/page-kits/:id |
POST |
/page-kits |
PUT |
/page-kits/:id |
Notifications, bugs, leads, export, SDK, agent
These are smaller utility namespaces:
/notifications— in-app notification queue per user/bugs— bug-report intake (used by the admin "Report a bug" affordance)/leads— public lead intake (no auth) + admin views (/api/leads)/export— bulk export of content / metadata in JSON/sdk— endpoints consumed by the JS SDK (snapshot fetches, etc.)/agent— agent / scoped-token management (in development; see Architecture: scoped agent tokens)
Sample requests
Create + deploy a Release
TOKEN=$(...)
SITE=site:my-site
# 1. Create a draft Release
curl -X POST https://app.staticowl.com/api/releases \
-H "Authorization: Bearer $TOKEN" -H "X-Site-Id: $SITE" \
-H "Content-Type: application/json" \
-d '{"name":"Spring sale","intent":"deploy","includes":[]}'
# → { "id": "release:abc", "state": "draft", ... }
# 2. Add content
curl -X POST .../api/releases/release:abc/includes \
-d '{"kind":"content","versionableId":"article:welcome","version":3,"at":"2026-04-28T..."}'
# 3. Submit for review
curl -X POST .../api/releases/release:abc/transition -d '{"state":"pending"}'
# 4. (Reviews run...)
# 5. Approve
curl -X POST .../api/releases/release:abc/transition -d '{"state":"approved"}'
# 6. Deploy
curl -X POST .../api/releases/release:abc/deploy \
-d '{"environmentId":"env:prod","intent":"deploy"}'
# → { "id": "deploy:xyz", "status": "pending" → "deployed", ... }
Schedule 12 posts across an 8-hour window
curl -X POST .../api/releases/drip-preview \
-d '{
"releaseIds": ["release:1", "release:2", ..., "release:12"],
"windowStart": "2026-05-01T08:00:00.000Z",
"windowEnd": "2026-05-01T16:00:00.000Z",
"pattern": "jittered",
"maxJitterMs": 600000,
"seed": 42
}'
# → { schedule: { timestamps: [...] }, pairs: [{releaseId, validFrom}, ...] }
# Operator reviews the calendar, then commits:
curl -X POST .../api/releases/drip-schedule \
-d '{ ... same body, plus "environmentId": "env:prod" }'
Replay the site as it was on Jan 15
curl ".../api/releases/replay?envId=env:prod&atTime=2026-01-15T15:00:00.000Z" \
-H "Authorization: Bearer $TOKEN" -H "X-Site-Id: $SITE"
# → { deployment: {...}, release: {...}, includes: [...] }
Env vars reference
See operations.md → Environment variables for the canonical list.