Skip to content

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.

  • 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

  1. Go to Admin → Settings → API Keys
  2. Click Create Key, give it a descriptive name, and set the type to CMS
  3. Copy the generated key — it’s shown only once

All requests require the X-API-Key header:

Terminal window
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.

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: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1706000000

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


Returns a paginated list of published posts.

Query parameters:

ParameterTypeDefaultDescription
localestringsite defaultFilter by locale (e.g. en, ja)
limitnumber10Results per page (max: 100)
offsetnumber0Pagination offset
sortstringdate:descSort field and direction (e.g. date:asc, title:asc)

Example:

Terminal window
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:

ParameterTypeDefaultDescription
include_bodybooleanfalseAccepted for forward compatibility. Body is not returned from the list endpoint — fetch /posts/:id for the full content.
body_formatstringmarkdownApplies 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"
}
}

Returns a single published post by ID, including the full article body.

Query parameters:

ParameterTypeDefaultDescription
localestringsite defaultLocale of the article to fetch
include_bodybooleantrueInclude full article body (default true for single post)
body_formatstringmarkdownBody format: markdown or html
include_relatedbooleanfalseInclude related posts array

Example:

Terminal window
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"]
}
}

Returns all categories with post counts.

Terminal window
curl "https://your-blog.com/api/cms/v1/categories?locale=en" \
-H "X-API-Key: your-api-key"

Returns posts in a specific category.

Query parameters: locale, limit, offset, sort

Terminal window
curl "https://your-blog.com/api/cms/v1/categories/Tech?locale=en&limit=10" \
-H "X-API-Key: your-api-key"

Returns all tags with post counts.


Returns posts with a specific tag.

Query parameters: locale, limit, offset


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:

ParameterTypeRequiredDescription
qstringYesSearch query
localestringNoFilter by locale
limitnumberNoResults per page (default: 10)
offsetnumberNoPagination offset

Example:

Terminal window
curl "https://your-blog.com/api/cms/v1/search?q=Next.js&locale=en&limit=5" \
-H "X-API-Key: your-api-key"

CLI API keys (cli:full permission) can also create, update, delete, and publish posts.

Create a new post. The request body must be a complete Markdown file including frontmatter.

Terminal window
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": "..." } }

Replace the full content of an existing post.

Terminal window
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"}'

Publish or unpublish a post by setting its status field.

Terminal window
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 a post permanently.

Terminal window
curl -X DELETE "https://your-blog.com/api/cms/v1/posts/my-post" \
-H "X-API-Key: your-cli-key"

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:

FieldTypeDescription
allbooleanRebuild the entire index (all articles, all locales)
articleIdstringSync a specific article (all locales)
localestringCombined with articleId — sync one article in one locale

Examples:

Terminal window
# Rebuild everything
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 '{"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 locale
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", "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.


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

FieldTypeDescription
fileFileImage file (jpg, png, gif, webp, svg)
pathstringDestination path, e.g. my-article/images/hero.jpg

Example:

Terminal window
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" } }

HTTP StatusMeaning
401 UnauthorizedMissing X-API-Key header
403 ForbiddenInvalid or expired API key, or insufficient permissions
404 Not FoundPost or resource does not exist
409 ConflictArticle ID already exists (on create)
429 Too Many RequestsRate limit exceeded
500 Internal Server ErrorServer-side error

Error responses use a structured JSON body:

{
"error": {
"code": "FORBIDDEN",
"message": "Invalid or expired API key"
}
}