HTTP API
RESTful endpoints for querying, creating, updating, and managing your content over HTTP.
The HTTP API gives you full CRUD access to documents, assets, and schemas. All document and asset endpoints require authentication via session or API key.
Base URL
All endpoints are relative to your app's origin:
https://your-app.com/api/...Reserved URLs
Three different layers handle requests under /api/*, and they're picked in this order:
1. SvelteKit handle hook → Better Auth (/api/auth/*)
2. SvelteKit filesystem router → specific +server.ts files
3. Catch-all (/api/[...slug]) → forwards into Hono → CMS routesSvelteKit's filesystem router prefers more-specific paths over rest parameters. Hono only sees what falls through the catch-all — it does not take precedence over a sibling +server.ts. That means if you create apps/studio/src/routes/api/documents/+server.ts, your file silently shadows the CMS's /api/documents handler and the CMS code never runs.
To wrap or replace a CMS route safely, use the api(app) config hook — it registers your handler inside Hono itself, where you can call await next() to chain to the built-in handler.
The full list of URLs the CMS owns:
Documents
| Method | Path | Notes |
|---|---|---|
GET | /api/documents | List with filters and pagination. |
POST | /api/documents | Create a draft. |
POST | /api/documents/query | Advanced query — read-only. |
GET | /api/documents/:id | Read a single document. |
PUT | /api/documents/:id | Update draft data. |
DELETE | /api/documents/:id | Delete (draft or both). |
POST | /api/documents/:id/publish | Publish draft to published. |
DELETE | /api/documents/:id/publish | Unpublish. |
GET | /api/documents/:id/versions | List versions. |
GET | /api/documents/:id/versions/:version | Read a specific version. |
POST | /api/documents/:id/versions/:version/restore | Restore a version into the draft. |
Assets
| Method | Path | Notes |
|---|---|---|
GET | /api/assets | List with filters. |
POST | /api/assets | Upload (multipart/form-data). |
DELETE | /api/assets/bulk | Bulk delete by id list. |
POST | /api/assets/references/counts | Reference counts for a list of asset ids. |
GET | /api/assets/:id | Read asset metadata. |
PATCH | /api/assets/:id | Update metadata (title, alt, credit, etc). |
DELETE | /api/assets/:id | Delete an asset. |
GET | /api/assets/:id/references | List documents referencing this asset. |
Organizations
| Method | Path | Notes |
|---|---|---|
GET | /api/organizations | List orgs the caller belongs to. |
POST | /api/organizations | Create an organization. |
POST | /api/organizations/switch | Switch active org context. |
GET | /api/organizations/members | List members of the active org. |
PATCH | /api/organizations/members | Update a member's role. |
DELETE | /api/organizations/members | Remove a member. |
POST | /api/organizations/invitations | Send an invitation. |
DELETE | /api/organizations/invitations | Cancel an invitation. |
GET | /api/organizations/:id | Read a specific org. |
PATCH | /api/organizations/:id | Update org name / slug / metadata. |
DELETE | /api/organizations/:id | Delete an org (super admin / owner). |
Roles
| Method | Path | Notes |
|---|---|---|
GET | /api/roles | List built-in + custom org roles. |
POST | /api/roles | Create a custom role. |
PATCH | /api/roles/:name | Update an existing role's capabilities. |
DELETE | /api/roles/:name | Delete a custom role. |
Schemas
| Method | Path | Notes |
|---|---|---|
GET | /api/schemas | All registered schemas (used by the studio shell). |
GET | /api/schemas/:type | A single schema. |
User account
| Method | Path | Notes |
|---|---|---|
PATCH | /api/user | Update profile (name, email). |
GET | /api/user/cms-preference | Read editor preferences (sidebar state, etc). |
PATCH | /api/user/cms-preference | Update editor preferences. |
POST | /api/user/request-password-reset | Trigger a password-reset email. |
POST | /api/user/reset-password | Complete a password reset with the emailed token. |
Outside Hono
| Path | Owned by |
|---|---|
/api/auth/* | Better Auth — handled by svelteKitHandler in hooks.server.ts, intercepts before the filesystem router runs. |
/api/graphql | The GraphQL endpoint. Configurable via graphql.path in aphex.config.ts. |
/api/instance-settings | Studio +server.ts (super-admin gated). See God Mode. |
/media/:id/:filename | Studio +server.ts — asset CDN handler. Lives at /media/*, not /api/media/*. |
Picking a safe path for custom endpoints
When you mount your own +server.ts under /api/*, avoid these prefixes: /api/auth, /api/documents, /api/assets, /api/organizations, /api/roles, /api/schemas, /api/user, /api/graphql, /api/instance-settings. The convention some teams adopt to be future-proof:
- Namespace custom routes under a stable prefix, e.g.
/api/app/*or/api/v1/*. - For one-off webhooks:
/api/webhooks/<provider>(Stripe, Slack, GitHub). - For internal tooling:
/api/admin/*or/api/internal/*.
If you ever need to claim a URL the CMS already owns — to wrap, replace, or extend a built-in handler — use the api(app) config hook. That's the only safe path to do so without forking cms-core.
Authentication
Include an API key in the x-api-key header:
curl -H "x-api-key: your_key_here" \
https://your-app.com/api/documents?type=postIf no x-api-key header is present, the API falls back to session authentication (cookies). When both are available, the API key takes precedence.
Each API key is scoped to the organization that was active when the key was created. All requests made with that key only see and modify data within that organization. See API Keys for details on organization scoping and parent–child hierarchy access.
Write protection
Mutating requests (POST, PUT, PATCH, DELETE) require write permission. API keys with only read permission receive a 403 response on mutations.
The one exception is POST /api/documents/query, which is treated as a read operation (it uses POST only because the filter payload can be complex).
Response format
Successful responses share a consistent envelope:
{
"success": true,
"data": { ... },
"pagination": { ... }
}Errors come in two shapes depending on which layer rejected the request:
// Validation / business errors (most 400s, 404s)
{
"success": false,
"error": "Bad Request",
"message": "Detailed error message",
"issues": [ ... ] // present when zod validation failed
}// Auth / permission middleware (401, 403)
{
"error": "Unauthorized"
}The middleware path is intentionally minimal — it short-circuits before the route handler, so it doesn't carry success or message. Treat both shapes as errors when the HTTP status is ≥ 400.
Documents
List documents
GET /api/documents?type={type}| Parameter | Type | Default | Description |
|---|---|---|---|
type | string | required | Document type (e.g. post, page). |
status | string | - | Filter by draft or published. |
perspective | string | 'draft' | Which version to return: 'draft' or 'published'. |
page | number | 1 | Page number. |
pageSize | number | 20 | Results per page. Alias: limit. |
sort | string | - | Sort field. Prefix with - for descending (e.g. -publishedAt). |
depth | number | 0 | Reference resolution depth (0–5). |
includeChildOrganizations | boolean | false | Include documents from child organizations. |
filterOrganizationIds | string | - | Comma-separated list of organization IDs to filter by. |
curl -H "x-api-key: your_key" \
"https://your-app.com/api/documents?type=post&perspective=published&pageSize=10&sort=-publishedAt"Response:
{
"success": true,
"data": [ ... ],
"pagination": {
"total": 42,
"page": 1,
"pageSize": 10,
"totalPages": 5,
"hasNextPage": true,
"hasPrevPage": false
}
}Get document by ID
GET /api/documents/{id}| Parameter | Type | Default | Description |
|---|---|---|---|
perspective | string | 'draft' | 'draft' or 'published'. |
depth | number | 0 | Reference resolution depth (0–5). |
curl -H "x-api-key: your_key" \
"https://your-app.com/api/documents/doc_123?perspective=published&depth=1"Create document
POST /api/documents{
"type": "post",
"data": {
"title": "My New Post",
"slug": "my-new-post",
"body": "Hello world."
},
"publish": false
}Returns 201 with the created document and validation results:
{
"success": true,
"data": { ... },
"validation": { "isValid": true, "errors": [] }
}Set "publish": true to publish immediately. This validates before publishing and returns 400 if validation fails.
Update document
PUT /api/documents/{id}{
"data": {
"title": "Updated Title"
},
"publish": false
}Delete document
DELETE /api/documents/{id}Publish document
POST /api/documents/{id}/publishValidates the draft and copies it to published data. Returns 400 if validation fails.
Unpublish document
DELETE /api/documents/{id}/publishReverts the document to draft-only state.
Versions
Every draft save and publish writes an entry to the document's version history. See Version History for the full guide.
List versions
GET /api/documents/{id}/versions?limit=25&offset=0Returns versions newest-first with the author resolved to a friendly createdByName.
Get a specific version
GET /api/documents/{id}/versions/{versionNumber}Restore a version
POST /api/documents/{id}/versions/{versionNumber}/restoreReplaces the document's draft with the snapshot data and writes a new draft-event version recording the restore. Published data is untouched until the editor publishes again.
Singletons
Singleton schemas are reachable through the same endpoints, with two differences:
- The list endpoint (
GET /api/documents?type=siteNavigation) always returns a one-element array — singletons ignore filters, pagination, andstatusparameters because there is at most one row. DELETE /api/documents/{id}returns400when called against the canonical singleton row.
Singletons are lazy-created on first read, so consumers never have to handle a 404.
Advanced querying
For complex filters that don't fit in query parameters, use the query endpoint:
POST /api/documents/queryThis is a read operation — API keys with read permission can use it.
{
"type": "post",
"where": {
"status": { "equals": "published" },
"title": { "contains": "tutorial" }
},
"limit": 20,
"page": 1,
"sort": ["-publishedAt", "title"],
"depth": 1,
"perspective": "published",
"includeChildOrganizations": true
}The where clause uses the same filter operators as the Local API.
Assets
List assets
GET /api/assets| Parameter | Type | Default | Description |
|---|---|---|---|
assetType | string | - | 'image' or 'file'. |
mimeType | string | - | Filter by MIME type (e.g. image/png). |
search | string | - | Search by title or description. |
limit | number | 20 | Results per page. |
offset | number | 0 | Number of results to skip. |
Response:
{
"success": true,
"data": [ ... ],
"pagination": {
"total": 85,
"page": 1,
"pageSize": 20,
"totalPages": 5,
"hasNextPage": true,
"hasPrevPage": false
}
}Upload asset
POST /api/assets
Content-Type: multipart/form-data| Field | Type | Description |
|---|---|---|
file | File | The file to upload. Required. |
title | string | Display title. |
description | string | Description. |
alt | string | Alt text (for images). |
creditLine | string | Credit/attribution. |
organizationId | string | Target organization. Defaults to the authenticated user's active org. |
curl -H "x-api-key: your_key" \
-F "[email protected]" \
-F "title=Hero Image" \
-F "alt=A sunset over the ocean" \
https://your-app.com/api/assetsGet asset
GET /api/assets/{id}Update asset metadata
PATCH /api/assets/{id}{
"title": "Updated Title",
"alt": "New alt text"
}Delete asset
DELETE /api/assets/{id}Returns 409 if the asset is still referenced by documents.
Bulk delete assets
DELETE /api/assets/bulk{
"ids": ["asset_id_1", "asset_id_2", "asset_id_3"]
}Response:
{
"success": true,
"data": {
"deleted": 2,
"failed": 0
}
}Returns 409 if any assets are still referenced, with the list of blocked IDs:
{
"success": false,
"error": "Cannot delete 1 asset because it is still referenced by documents",
"referencedIds": ["asset_id_2"]
}Find asset references
GET /api/assets/{id}/referencesReturns which documents reference a given asset:
{
"success": true,
"data": {
"references": [ ... ],
"total": 3
}
}Batch reference counts
POST /api/assets/references/countsGet reference counts for multiple assets in one request. Useful for checking which assets are safe to delete.
{
"ids": ["asset_id_1", "asset_id_2", "asset_id_3"]
}Response:
{
"success": true,
"data": {
"asset_id_1": 2,
"asset_id_2": 0,
"asset_id_3": 1
}
}Schemas
List all schemas
GET /api/schemasGet schema by type
GET /api/schemas/{type}Roles
Roles let an organization map names (owner, admin, custom roles like Publisher) to sets of capabilities. All role endpoints require a session — API keys cannot manage roles.
List roles
GET /api/rolesReturns every role defined for the active organization, including the four built-ins.
{
"success": true,
"data": [
{
"id": "role_abc",
"organizationId": "org_123",
"name": "owner",
"description": "Full access including organization deletion.",
"capabilities": ["document.read", "document.create", "…"],
"isBuiltIn": true,
"createdAt": "2025-01-10T00:00:00Z",
"updatedAt": "2025-01-10T00:00:00Z"
}
]
}Create a custom role
POST /api/rolesRequires the role.manage capability. Built-in names (owner, admin, editor, viewer) are reserved.
{
"name": "Publisher",
"description": "Can edit and publish but not delete.",
"capabilities": [
"document.read",
"document.create",
"document.update",
"document.publish",
"document.unpublish",
"asset.read",
"asset.upload"
]
}Write capabilities auto-include their matching read cap, so you don't need to list document.read alongside document.create manually (it's inserted on intake).
Update a role
PATCH /api/roles/{name}Requires role.manage. Both built-in and custom roles can be edited — at least one of description or capabilities must be provided.
{
"capabilities": ["document.read", "asset.read", "member.invite"]
}Delete a custom role
DELETE /api/roles/{name}Requires role.manage. Returns 403 for built-in names and 409 if the role is still assigned to any member or pending invitation.
Status codes
| Code | Meaning |
|---|---|
200 | Success (GET, PUT, PATCH). |
201 | Created (POST). |
400 | Bad request — missing parameters, invalid data, or validation failure. |
401 | Unauthorized — no valid session or API key. |
403 | Forbidden — insufficient permissions (e.g. viewer trying to write, or read-only API key on a mutation). |
404 | Not found. |
409 | Conflict — e.g. asset still has references. |
500 | Server error. |
Last updated on