Aphex

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 routes

SvelteKit'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

MethodPathNotes
GET/api/documentsList with filters and pagination.
POST/api/documentsCreate a draft.
POST/api/documents/queryAdvanced query — read-only.
GET/api/documents/:idRead a single document.
PUT/api/documents/:idUpdate draft data.
DELETE/api/documents/:idDelete (draft or both).
POST/api/documents/:id/publishPublish draft to published.
DELETE/api/documents/:id/publishUnpublish.
GET/api/documents/:id/versionsList versions.
GET/api/documents/:id/versions/:versionRead a specific version.
POST/api/documents/:id/versions/:version/restoreRestore a version into the draft.

Assets

MethodPathNotes
GET/api/assetsList with filters.
POST/api/assetsUpload (multipart/form-data).
DELETE/api/assets/bulkBulk delete by id list.
POST/api/assets/references/countsReference counts for a list of asset ids.
GET/api/assets/:idRead asset metadata.
PATCH/api/assets/:idUpdate metadata (title, alt, credit, etc).
DELETE/api/assets/:idDelete an asset.
GET/api/assets/:id/referencesList documents referencing this asset.

Organizations

MethodPathNotes
GET/api/organizationsList orgs the caller belongs to.
POST/api/organizationsCreate an organization.
POST/api/organizations/switchSwitch active org context.
GET/api/organizations/membersList members of the active org.
PATCH/api/organizations/membersUpdate a member's role.
DELETE/api/organizations/membersRemove a member.
POST/api/organizations/invitationsSend an invitation.
DELETE/api/organizations/invitationsCancel an invitation.
GET/api/organizations/:idRead a specific org.
PATCH/api/organizations/:idUpdate org name / slug / metadata.
DELETE/api/organizations/:idDelete an org (super admin / owner).

Roles

MethodPathNotes
GET/api/rolesList built-in + custom org roles.
POST/api/rolesCreate a custom role.
PATCH/api/roles/:nameUpdate an existing role's capabilities.
DELETE/api/roles/:nameDelete a custom role.

Schemas

MethodPathNotes
GET/api/schemasAll registered schemas (used by the studio shell).
GET/api/schemas/:typeA single schema.

User account

MethodPathNotes
PATCH/api/userUpdate profile (name, email).
GET/api/user/cms-preferenceRead editor preferences (sidebar state, etc).
PATCH/api/user/cms-preferenceUpdate editor preferences.
POST/api/user/request-password-resetTrigger a password-reset email.
POST/api/user/reset-passwordComplete a password reset with the emailed token.

Outside Hono

PathOwned by
/api/auth/*Better Auth — handled by svelteKitHandler in hooks.server.ts, intercepts before the filesystem router runs.
/api/graphqlThe GraphQL endpoint. Configurable via graphql.path in aphex.config.ts.
/api/instance-settingsStudio +server.ts (super-admin gated). See God Mode.
/media/:id/:filenameStudio +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=post

If 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}
ParameterTypeDefaultDescription
typestringrequiredDocument type (e.g. post, page).
statusstring-Filter by draft or published.
perspectivestring'draft'Which version to return: 'draft' or 'published'.
pagenumber1Page number.
pageSizenumber20Results per page. Alias: limit.
sortstring-Sort field. Prefix with - for descending (e.g. -publishedAt).
depthnumber0Reference resolution depth (0–5).
includeChildOrganizationsbooleanfalseInclude documents from child organizations.
filterOrganizationIdsstring-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}
ParameterTypeDefaultDescription
perspectivestring'draft''draft' or 'published'.
depthnumber0Reference 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}/publish

Validates the draft and copies it to published data. Returns 400 if validation fails.

Unpublish document

DELETE /api/documents/{id}/publish

Reverts 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=0

Returns 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}/restore

Replaces 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, and status parameters because there is at most one row.
  • DELETE /api/documents/{id} returns 400 when 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/query

This 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
ParameterTypeDefaultDescription
assetTypestring-'image' or 'file'.
mimeTypestring-Filter by MIME type (e.g. image/png).
searchstring-Search by title or description.
limitnumber20Results per page.
offsetnumber0Number 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
FieldTypeDescription
fileFileThe file to upload. Required.
titlestringDisplay title.
descriptionstringDescription.
altstringAlt text (for images).
creditLinestringCredit/attribution.
organizationIdstringTarget 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/assets

Get 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}/references

Returns which documents reference a given asset:

{
  "success": true,
  "data": {
    "references": [ ... ],
    "total": 3
  }
}

Batch reference counts

POST /api/assets/references/counts

Get 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/schemas

Get 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/roles

Returns 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/roles

Requires 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

CodeMeaning
200Success (GET, PUT, PATCH).
201Created (POST).
400Bad request — missing parameters, invalid data, or validation failure.
401Unauthorized — no valid session or API key.
403Forbidden — insufficient permissions (e.g. viewer trying to write, or read-only API key on a mutation).
404Not found.
409Conflict — e.g. asset still has references.
500Server error.
Edit on GitHub

Last updated on