Headless CMS API
Baan’s Headless CMS API lets you deliver your content to any frontend — a mobile app, a separate Next.js site, a React Native app, or anything that can make HTTP requests. The API is versioned and authenticated with API keys. Read-only CMS keys support content delivery; CLI keys additionally support creating, updating, publishing, and deleting posts.
What You Can Build
Section titled “What You Can Build”- A mobile app that reads your blog content
- A multi-site setup where one Baan instance serves multiple frontends
- An integration with a newsletter tool, aggregator, or custom search interface
- An AI assistant that reads and summarizes your articles
Create an API Key
Section titled “Create an API Key”- Go to Admin → Settings → API Keys
- Click Create Key, give it a descriptive name, and set the type to CMS
- Copy the generated key — it’s shown only once
Make a Request
Section titled “Make a Request”All requests require the X-API-Key header:
curl https://your-blog.com/api/cms/v1/posts \ -H "X-API-Key: your-api-key"Keys are compared using constant-time comparison to prevent timing attacks.
Rate Limiting
Section titled “Rate Limiting”The default rate limit is 300 requests per hour per API key. Each response includes headers so you can track your usage:
X-RateLimit-Limit: 300X-RateLimit-Remaining: 287X-RateLimit-Reset: 1706000000The rate limit is configurable per key in Admin → Settings → API Keys. Click the rate limit value in the table to edit it inline (range: 1–100,000). A warning is shown for values above 1,000.
Rate limit counters are stored in the database, so the limit is enforced accurately across multiple instances — both serverless deployments (Vercel) and multi-container Docker setups.
Endpoints
Section titled “Endpoints”GET /api/cms/v1/posts
Section titled “GET /api/cms/v1/posts”Returns a paginated list of published posts.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
locale | string | site default | Filter by locale (e.g. en, ja) |
limit | number | 10 | Results per page (max: 100) |
offset | number | 0 | Pagination offset |
sort | string | date:desc | Sort field and direction (e.g. date:asc, title:asc) |
Example:
curl "https://your-blog.com/api/cms/v1/posts?locale=en&limit=5&offset=0" \ -H "X-API-Key: your-api-key"Additional query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
include_body | boolean | false | Accepted for forward compatibility. Body is not returned from the list endpoint — fetch /posts/:id for the full content. |
body_format | string | markdown | Applies to the detail endpoint: markdown (raw) or html (rendered) |
Response:
{ "data": [ { "id": "getting-started-nextjs", "slug": "getting-started-nextjs", "title": "Getting Started with Next.js", "date": "2025-01-15", "category": "Tech", "tags": ["Next.js", "React"], "excerpt": "A beginner's guide to Next.js App Router.", "featured_image": "https://storage.example.com/getting-started-nextjs/images/hero.jpg", "locale": "en", "author": null, "reading_time": 3, "created_at": "2025-01-15", "updated_at": "2025-01-15" } ], "meta": { "total": 42, "limit": 5, "offset": 0, "has_more": true, "locale": "en" }}GET /api/cms/v1/posts/:id
Section titled “GET /api/cms/v1/posts/:id”Returns a single published post by ID, including the full article body.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
locale | string | site default | Locale of the article to fetch |
include_body | boolean | true | Include full article body (default true for single post) |
body_format | string | markdown | Body format: markdown or html |
include_related | boolean | false | Include related posts array |
Example:
curl "https://your-blog.com/api/cms/v1/posts/getting-started-nextjs?locale=en" \ -H "X-API-Key: your-api-key"Response:
{ "data": { "id": "getting-started-nextjs", "slug": "getting-started-nextjs", "title": "Getting Started with Next.js", "date": "2025-01-15", "category": "Tech", "tags": ["Next.js", "React"], "excerpt": "A beginner's guide to Next.js App Router.", "featured_image": "https://storage.example.com/.../hero.jpg", "locale": "en", "author": null, "reading_time": 5, "created_at": "2025-01-15", "updated_at": "2025-01-15", "body": "# Getting Started with Next.js\n\nNext.js is a React framework...", "available_locales": ["en"] }}GET /api/cms/v1/categories
Section titled “GET /api/cms/v1/categories”Returns all categories with post counts.
curl "https://your-blog.com/api/cms/v1/categories?locale=en" \ -H "X-API-Key: your-api-key"GET /api/cms/v1/categories/:name
Section titled “GET /api/cms/v1/categories/:name”Returns posts in a specific category.
Query parameters: locale, limit, offset, sort
curl "https://your-blog.com/api/cms/v1/categories/Tech?locale=en&limit=10" \ -H "X-API-Key: your-api-key"GET /api/cms/v1/tags
Section titled “GET /api/cms/v1/tags”Returns all tags with post counts.
GET /api/cms/v1/tags/:name
Section titled “GET /api/cms/v1/tags/:name”Returns posts with a specific tag.
Query parameters: locale, limit, offset
GET /api/cms/v1/search
Section titled “GET /api/cms/v1/search”Full-text search across published posts. Queries the article metadata index and supports CJK languages (Japanese, Chinese, Korean) without requiring spaces between words.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | Search query |
locale | string | No | Filter by locale |
limit | number | No | Results per page (default: 10) |
offset | number | No | Pagination offset |
Example:
curl "https://your-blog.com/api/cms/v1/search?q=Next.js&locale=en&limit=5" \ -H "X-API-Key: your-api-key"Write Endpoints (CLI keys only)
Section titled “Write Endpoints (CLI keys only)”CLI API keys (cli:full permission) can also create, update, delete, and publish posts.
POST /api/cms/v1/posts
Section titled “POST /api/cms/v1/posts”Create a new post. The request body must be a complete Markdown file including frontmatter.
curl -X POST "https://your-blog.com/api/cms/v1/posts" \ -H "X-API-Key: your-cli-key" \ -H "Content-Type: application/json" \ -d '{"article_id": "my-new-post", "content": "---\ntitle: My Post\n...\n---\nBody here.", "locale": "en"}'Returns 201 Created on success: { "data": { "id": "my-new-post", "path": "..." } }
PUT /api/cms/v1/posts/:id
Section titled “PUT /api/cms/v1/posts/:id”Replace the full content of an existing post.
curl -X PUT "https://your-blog.com/api/cms/v1/posts/my-post" \ -H "X-API-Key: your-cli-key" \ -H "Content-Type: application/json" \ -d '{"content": "---\ntitle: Updated\n---\nNew body.", "locale": "en"}'POST /api/cms/v1/posts/:id/publish
Section titled “POST /api/cms/v1/posts/:id/publish”Publish or unpublish a post by setting its status field.
curl -X POST "https://your-blog.com/api/cms/v1/posts/my-post/publish" \ -H "X-API-Key: your-cli-key" \ -H "Content-Type: application/json" \ -d '{"status": "published"}'DELETE /api/cms/v1/posts/:id
Section titled “DELETE /api/cms/v1/posts/:id”Delete a post permanently.
curl -X DELETE "https://your-blog.com/api/cms/v1/posts/my-post" \ -H "X-API-Key: your-cli-key"POST /api/cms/v1/index/sync
Section titled “POST /api/cms/v1/index/sync”Sync the article metadata index from storage to the database. Use this after bulk-importing articles via CLI, or to repair a broken index.
Request body:
| Field | Type | Description |
|---|---|---|
all | boolean | Rebuild the entire index (all articles, all locales) |
articleId | string | Sync a specific article (all locales) |
locale | string | Combined with articleId — sync one article in one locale |
Examples:
# Rebuild everythingcurl -X POST "https://your-blog.com/api/cms/v1/index/sync" \ -H "X-API-Key: your-cli-key" \ -H "Content-Type: application/json" \ -d '{"all": true}'
# Sync one article (all locales)curl -X POST "https://your-blog.com/api/cms/v1/index/sync" \ -H "X-API-Key: your-cli-key" \ -H "Content-Type: application/json" \ -d '{"articleId": "my-post"}'
# Sync one article, one localecurl -X POST "https://your-blog.com/api/cms/v1/index/sync" \ -H "X-API-Key: your-cli-key" \ -H "Content-Type: application/json" \ -d '{"articleId": "my-post", "locale": "ja"}'Response:
{ "data": { "synced": 9, "message": "Rebuilt all: 9 entries" } }The same operation is available in the Admin UI at Settings → System → Rebuild Article Index.
POST /api/cms/v1/media
Section titled “POST /api/cms/v1/media”Upload an image file. Requires a CLI API key (cli:full permission).
The uploaded file is stored at {article-id}/images/{filename} in the configured storage backend and is immediately available for use in posts.
Request: multipart/form-data
| Field | Type | Description |
|---|---|---|
file | File | Image file (jpg, png, gif, webp, svg) |
path | string | Destination path, e.g. my-article/images/hero.jpg |
Example:
curl -X POST "https://your-blog.com/api/cms/v1/media" \ -H "X-API-Key: your-cli-key" \ -F "file=@hero.jpg" \ -F "path=my-article/images/hero.jpg"Response:
{ "data": { "path": "my-article/images/hero.jpg" } }Error Responses
Section titled “Error Responses”| HTTP Status | Meaning |
|---|---|
401 Unauthorized | Missing X-API-Key header |
403 Forbidden | Invalid or expired API key, or insufficient permissions |
404 Not Found | Post or resource does not exist |
409 Conflict | Article ID already exists (on create) |
429 Too Many Requests | Rate limit exceeded |
500 Internal Server Error | Server-side error |
Error responses use a structured JSON body:
{ "error": { "code": "FORBIDDEN", "message": "Invalid or expired API key" }}