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
Authentication
Section titled “Authentication”JWT login — POST /auth/login
Section titled “JWT login — POST /auth/login”{ "email": "admin@wollycms.local", "password": "admin123" }Returns { "data": { "token": "eyJ...", "user": { "id": 1, "email": "...", "name": "Admin", "role": "admin" } } }.
Use the token: Authorization: Bearer <token>
API keys
Section titled “API keys”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 |
Current user — GET /auth/me
Section titled “Current user — GET /auth/me”| 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 |
Blocks
Section titled “Blocks”| 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 |
Content Types (admin role)
Section titled “Content Types (admin role)”| 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 } ]}Block Types (admin role)
Section titled “Block Types (admin role)”| 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 |
Taxonomies
Section titled “Taxonomies”| 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 |
Users (admin role)
Section titled “Users (admin role)”| 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.
Export / Import
Section titled “Export / Import”Full site export and import for backups, migrations, and staging data sync. Requires admin role.
Export — GET /export
Section titled “Export — GET /export”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.
curl -s "https://your-cms.example.com/api/admin/export" \ -H "X-API-Key: sk_..." \ -o backup.jsonResponse: JSON file with version: 2 and all content tables.
Import — POST /import
Section titled “Import — POST /import”Imports content from a JSON export. Supports both v1 (legacy) and v2 (full) exports. Uses slug/ID-based deduplication — existing records are not overwritten.
curl -X POST "https://your-cms.example.com/api/admin/import" \ -H "X-API-Key: sk_..." \ -H "Content-Type: application/json" \ -d @backup.jsonResponse: { "data": { "imported": true, "stats": { "pages": 42, "blocks": 120, ... } } }
Error format
Section titled “Error format”{ "errors": [{ "code": "VALIDATION", "message": "Title is required", "path": ["title"] }] }Codes: VALIDATION, NOT_FOUND, CONFLICT, UNAUTHORIZED, FORBIDDEN, IN_USE, INTERNAL_ERROR.
Webhooks
Section titled “Webhooks”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.