Skip to content

Admin API

The admin API provides full CRUD operations for all CMS entities. All endpoints (except login) require authentication.

Base URL: http://localhost:4321/api/admin

{ "email": "admin@wollycms.local", "password": "admin123" }

Returns { "data": { "token": "eyJ...", "user": { "id": 1, "email": "...", "name": "Admin", "role": "admin" } } }.

Use the token: Authorization: Bearer <token>

For programmatic access, create API keys at POST /api-keys:

{ "name": "CI/CD Pipeline", "permissions": "content:write" }

The key (sk_...) is returned once. Use via X-API-Key header or Authorization: Bearer sk_....

Permission Role Access
content:read viewer Read-only
content:write editor Read + write content
* or admin:* admin Full access
Role Can do
viewer Read all admin data
editor CRUD pages, blocks, menus, media, taxonomies
admin Everything + manage schemas, users, API keys
Endpoint Description
GET /pages List all pages (any status). Filters: ?type=, ?status=, ?search=, ?sort=, ?limit=, ?offset=
GET /pages/:id Get page by ID with resolved blocks
POST /pages Create page. Body: { title, slug?, slugOverride?, typeId, status, fields, metaTitle?, metaDescription?, scheduledAt? }. If the content type has a slugPrefix configured, slug is auto-prefixed unless slugOverride: true. See Slug prefixes.
POST /pages/upsert Create or update by slug. Returns { data, created: bool }
PUT /pages/:id Partial update. Auto-creates revision. Supports revisionNote. Setting slugOverride re-validates the slug against the content type’s prefix.
DELETE /pages/:id Delete page
POST /pages/bulk Bulk action. Body: { ids: [1,2], action: "publish" }. Actions: publish, unpublish, archive, delete
Endpoint Description
GET /blocks List reusable blocks. Filters: ?type=, ?search=, ?reusable=false
GET /blocks/:id Get block with usage info
POST /blocks Create. Body: { typeId, title, fields, isReusable? }
PUT /blocks/:id Update block
DELETE /blocks/:id Delete (fails with 409 if still in use)
Endpoint Description
GET /menus List all menus
GET /menus/:id Get menu with item tree
POST /menus Create. Body: { name, slug }
PUT /menus/:id Update menu
DELETE /menus/:id Delete (cascades items)
POST /menus/:id/items Add item. Body: { title, url?, pageId?, parentId?, target?, position?, depth? }
PUT /menus/:id/items/:itemId Update item
DELETE /menus/:id/items/:itemId Delete item
PUT /menus/:id/items-order Reorder. Body: { items: [{ id, parentId, position, depth }] }
Endpoint Description
GET /media List media. Filters: ?type=, ?search=, ?folder=, ?sort=, ?order=
GET /media/folders List distinct folders
GET /media/:id Get single media with URLs
POST /media Upload. Multipart: file (required), title, altText, folder, variant_thumbnail, variant_medium, variant_large (optional pre-generated WebP variants). Max 50 MB
PUT /media/:id Update metadata: { altText?, title?, folder?, metadata? }
DELETE /media/:id Delete file and all variants from storage
Endpoint Description
GET /content-types List all
GET /content-types/:id Get one
POST /content-types Create. Body: { name, slug, fieldsSchema, regions, defaultBlocks?, settings?, description? }. Use settings.slugPrefix to require a URL prefix on all pages of this type.
PUT /content-types/:id Update. Enabling settings.slugPrefix non-destructively marks any non-matching existing pages as slugOverride=true; the response includes meta.sweptOverrides with the count.
DELETE /content-types/:id Delete

The defaultBlocks field is an array of block definitions that are auto-created when a new page of this type is made:

{
"defaultBlocks": [
{ "region": "content", "blockTypeSlug": "rich_text", "position": 0 },
{ "region": "sidebar", "blockTypeSlug": "link_list", "position": 0 }
]
}
Endpoint Description
GET /block-types List all
GET /block-types/:id Get one
POST /block-types Create. Body: { name, slug, fieldsSchema, icon?, description? }
PUT /block-types/:id Update
DELETE /block-types/:id Delete
Endpoint Description
GET /taxonomies List all
GET /taxonomies/:id Get with terms tree
POST /taxonomies Create. Body: { name, slug, hierarchical?, description? }
PUT /taxonomies/:id Update
DELETE /taxonomies/:id Delete (cascades terms)
POST /taxonomies/:id/terms Add term: { name, slug, parentId?, weight?, fields? }
PUT /taxonomies/:id/terms/:termId Update term
DELETE /taxonomies/:id/terms/:termId Delete term
Endpoint Description
GET /users List all (no password hashes)
POST /users Create. Body: { email, name, password, role? }. Min 8 char password
PUT /users/:id Update (password optional)
DELETE /users/:id Delete (cannot delete yourself or last admin)

Roles: admin, editor, viewer.

Full site export and import for backups, migrations, and staging data sync. Requires admin role.

Returns a JSON file containing all site content and configuration. Use for backups, site migration, or syncing content to a staging environment.

Included: content types, block types, pages, blocks, page blocks, page revisions, taxonomies, terms, content-term assignments, menus, menu items, redirects, media metadata, site config, tracking scripts, webhooks.

Not included (recreate on target): users, API keys, OAuth tokens, 2FA settings, audit logs.

Not included (copy separately): media files (R2/S3 storage). The export contains media metadata (filenames, paths, dimensions) but not the actual binary files.

Terminal window
curl -s "https://your-cms.example.com/api/admin/export" \
-H "X-API-Key: sk_..." \
-o backup.json

Response: JSON file with version: 2 and all content tables.

Imports content from a JSON export. Supports both v1 (legacy) and v2 (full) exports. Uses slug/ID-based deduplication — existing records are not overwritten.

Terminal window
curl -X POST "https://your-cms.example.com/api/admin/import" \
-H "X-API-Key: sk_..." \
-H "Content-Type: application/json" \
-d @backup.json

Response: { "data": { "imported": true, "stats": { "pages": 42, "blocks": 120, ... } } }

{ "errors": [{ "code": "VALIDATION", "message": "Title is required", "path": ["title"] }] }

Codes: VALIDATION, NOT_FOUND, CONFLICT, UNAUTHORIZED, FORBIDDEN, IN_USE, INTERNAL_ERROR.

Events fired on content changes: page.created, page.updated, page.published, page.unpublished, page.deleted, media.uploaded, media.deleted. Configure webhook URLs at GET/POST /webhooks.