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_....

PermissionRoleAccess
content:readviewerRead-only
content:writeeditorRead + write content
* or admin:*adminFull access
RoleCan do
viewerRead all admin data
editorCRUD pages, blocks, menus, media, taxonomies
adminEverything + manage schemas, users, API keys
EndpointDescription
GET /pagesList all pages (any status). Filters: ?type=, ?status=, ?search=, ?sort=, ?limit=, ?offset=
GET /pages/:idGet page by ID with resolved blocks
POST /pagesCreate page. Body: { title, slug?, typeId, status, fields, metaTitle?, metaDescription?, scheduledAt? }
POST /pages/upsertCreate or update by slug. Returns { data, created: bool }
PUT /pages/:idPartial update. Auto-creates revision. Supports revisionNote
DELETE /pages/:idDelete page
POST /pages/bulkBulk action. Body: { ids: [1,2], action: "publish" }. Actions: publish, unpublish, archive, delete
EndpointDescription
GET /blocksList reusable blocks. Filters: ?type=, ?search=, ?reusable=false
GET /blocks/:idGet block with usage info
POST /blocksCreate. Body: { typeId, title, fields, isReusable? }
PUT /blocks/:idUpdate block
DELETE /blocks/:idDelete (fails with 409 if still in use)
EndpointDescription
GET /menusList all menus
GET /menus/:idGet menu with item tree
POST /menusCreate. Body: { name, slug }
PUT /menus/:idUpdate menu
DELETE /menus/:idDelete (cascades items)
POST /menus/:id/itemsAdd item. Body: { title, url?, pageId?, parentId?, target?, position?, depth? }
PUT /menus/:id/items/:itemIdUpdate item
DELETE /menus/:id/items/:itemIdDelete item
PUT /menus/:id/items-orderReorder. Body: { items: [{ id, parentId, position, depth }] }
EndpointDescription
GET /mediaList media. Filters: ?type=, ?search=, ?folder=, ?sort=, ?order=
GET /media/foldersList distinct folders
GET /media/:idGet single media with URLs
POST /mediaUpload. Multipart: file (required), title, altText, folder. Max 50 MB
PUT /media/:idUpdate metadata: { altText?, title?, folder?, metadata? }
DELETE /media/:idDelete file and all variants from storage
EndpointDescription
GET /content-typesList all
GET /content-types/:idGet one
POST /content-typesCreate. Body: { name, slug, fieldsSchema, regions, description? }
PUT /content-types/:idUpdate
DELETE /content-types/:idDelete
EndpointDescription
GET /block-typesList all
GET /block-types/:idGet one
POST /block-typesCreate. Body: { name, slug, fieldsSchema, icon?, description? }
PUT /block-types/:idUpdate
DELETE /block-types/:idDelete
EndpointDescription
GET /taxonomiesList all
GET /taxonomies/:idGet with terms tree
POST /taxonomiesCreate. Body: { name, slug, hierarchical?, description? }
PUT /taxonomies/:idUpdate
DELETE /taxonomies/:idDelete (cascades terms)
POST /taxonomies/:id/termsAdd term: { name, slug, parentId?, weight?, fields? }
PUT /taxonomies/:id/terms/:termIdUpdate term
DELETE /taxonomies/:id/terms/:termIdDelete term
EndpointDescription
GET /usersList all (no password hashes)
POST /usersCreate. Body: { email, name, password, role? }. Min 8 char password
PUT /users/:idUpdate (password optional)
DELETE /users/:idDelete (cannot delete yourself or last admin)

Roles: admin, editor, viewer.

{ "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.