REST API
Complete reference for all API endpoints.
CMS exposes a REST API at /api for all CRUD operations, authentication, and media uploads.
All endpoints require authentication by default. Only /api/auth/login, /api/auth/logout, and /api/health are accessible without a session or API key. To make collection data publicly accessible, create an API key with the public role — see API Keys.
Collection endpoints
Every entity gets these endpoints automatically:
GET /api/:collection
GET /api/:collection/:id
POST /api/:collection
PUT /api/:collection/:id
DELETE /api/:collection/:id
List records
GET /api/posts?filter[status]=published&sort=-createdAt&limit=10
Response (200):
{
"data": [
{ "id": "...", "title": "...", "status": "published" }
],
"meta": {
"total": 42,
"limit": 10,
"offset": 0
}
}
Get single record
GET /api/posts/uuid-here?resolve[author]=name,bio
Response (200):
{
"data": {
"id": "uuid-here",
"title": "My Post",
"author": {
"id": "author-uuid",
"name": "Pedro",
"bio": "Design engineer"
}
}
}
Returns 404 if the record doesn’t exist.
Create record
POST /api/posts
Content-Type: application/json
{
"title": "New Post",
"slug": "/blog/new-post",
"status": "draft"
}
Response (201):
{
"data": {
"id": "new-uuid",
"title": "New Post",
"slug": "/blog/new-post",
"status": "draft",
"createdAt": "2025-06-01T00:00:00Z",
"updatedAt": "2025-06-01T00:00:00Z"
}
}
Update record
PUT /api/posts/uuid-here
Content-Type: application/json
{
"title": "Updated Title"
}
Response (200): Returns the full updated record. Only send the fields you want to change.
Delete record
DELETE /api/posts/uuid-here
Response (200):
{
"data": { "id": "uuid-here" }
}
Deletes the record and any related junction table rows.
Globals endpoints
Singleton entities get two endpoints instead of the standard collection endpoints:
GET /api/globals/:name
PUT /api/globals/:name
Get global
GET /api/globals/site-settings
Response (200):
{
"data": {
"id": "uuid",
"siteName": "My Site",
"siteDescription": "...",
"logo": "media-uuid",
"createdAt": "...",
"updatedAt": "..."
}
}
Update global
PUT /api/globals/site-settings
Content-Type: application/json
{ "siteName": "New Name" }
Response (200): Returns the full updated record.
Authentication endpoints
POST /api/auth/login
POST /api/auth/logout
GET /api/auth/me
POST /api/auth/refresh
Login
POST /api/auth/login
Content-Type: application/json
{
"email": "admin@example.com",
"password": "secret"
}
Response (200):
{
"data": {
"user": {
"id": "user-uuid",
"email": "admin@example.com",
"name": "Admin",
"role": "admin"
},
"expiresAt": "2025-06-08T00:00:00Z"
}
}
Sets an cms_session HTTP-only cookie. Sessions last 7 days.
API token authentication
You can authenticate API requests with a Bearer token instead of a session cookie:
Authorization: Bearer <token>
If a Bearer token is present, it must be valid — the server returns 401 immediately on an invalid token. There is no fallback to the session cookie.
Logout
POST /api/auth/logout
Clears the session cookie.
Current user
GET /api/auth/me
Returns the authenticated user and their role. Returns 401 if not authenticated.
Refresh session
POST /api/auth/refresh
Extends the session expiration. Returns the new expiresAt.
API key authentication
For headless use (SSGs, CI/CD, server-to-server), use a Bearer token instead of a session cookie:
GET /api/posts?filter[status]=published
Authorization: Bearer cms_aB3xK9mNpQ2rS7vW1yZ...
Keys are created from the admin UI at Settings → API Keys or via POST /api/_api-keys (admin session required). See the API Keys reference for details.
Translation endpoint
For translatable entities:
POST /api/pages/uuid-here/translate
Content-Type: application/json
{ "locale": "pt" }
Creates a translation of the record in the target locale.
Media upload
POST /api/_media/upload
Content-Type: multipart/form-data
file: <binary>
alt: "Optional description"
Returns the media record with image dimensions (if applicable). Deduplicates by file hash.
Image transforms
GET /api/assets/:filename?w=400&format=webp
GET /api/assets/:filename?w=800&fit=cover&format=avif&q=75
Serves the original file with no query params, or a transformed image with w, h, fit, format, and q parameters. See Media guide for full parameter reference.
Health check
GET /api/health
Returns 200 when healthy:
{ "status": "ok", "db": "ok" }
Returns 503 when the database is unreachable:
{ "status": "degraded", "db": "unreachable" }
Admin config
GET /api/admin/config
Returns the full CMS configuration for the admin UI (entity definitions, field types, locales, admin config). Stripped of hooks and internals.
Rate limiting
All endpoints are rate-limited per IP:
| Scope | Limit |
|---|---|
| General API | 200 requests/minute |
Auth endpoints (/api/auth/*) | 10 requests/minute |
When exceeded, the server returns 429 Too Many Requests with a Retry-After header.
Rate limiting is in-memory and per-process — it is not shared across multiple instances. For production deployments with multiple processes, use a reverse-proxy rate limiter (nginx, Cloudflare) in front of the CMS.
Error responses
All errors follow a consistent format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "field": "title", "message": "Required" },
{ "field": "slug", "message": "Must be unique" }
]
}
}
Error codes
| Code | HTTP Status | When |
|---|---|---|
VALIDATION_ERROR | 400 | Invalid input data |
UNAUTHORIZED | 401 | Missing or invalid session |
FORBIDDEN | 403 | Role lacks permission |
NOT_FOUND | 404 | Record or collection not found |
CONFLICT | 409 | Unique constraint violation |
TOO_MANY_REQUESTS | 429 | Rate limit exceeded |
INTERNAL_ERROR | 500 | Unexpected server error |
Previous
Deployment
Next
Field Types Reference