owlCMS API
The owlCMS API lets you programmatically access your content. Use it to build websites, mobile apps, or integrate with any service. All responses are JSON.
Overview
The owlCMS API is a RESTful JSON API following a headless architecture — we manage your data while you maintain full control over your frontend design and implementation.
Base URL
https://owlcms.com/v1Best Practices & SEO
Choosing the right rendering strategy is critical for performance and search visibility.
SSG — Static Site Generation
RecommendedUsed by: Astro, Next.js (Static), Hugo, Jekyll. Your site is built once at build time. It fetches all posts from owlCMS and generates a real .html file for every post. Since the HTML already contains your content, Google sees it instantly. Fastest and most SEO-friendly.
SSR — Server-Side Rendering
DynamicUsed by: Next.js, Remix, Nuxt, SvelteKit. The server generates HTML on every request, fetching fresh data from owlCMS each time. Best when your content changes every few minutes and you can't wait for a build step.
CSR — Client-Side Rendering
Not for SEOUsed by: Plain Vite, Create React App, Vue CLI. The server sends a blank HTML file and a JS bundle. The browser fetches data and builds the page — but search crawlers often see a blank screen.
Social media platforms (Facebook, Twitter) cannot read your post titles or images. Avoid CSR for any public content.
Development mode may not render correctly
When running next dev or any framework's dev server, the page source is bloated with HMR scripts, devtools, and debug payloads. This can make the HTML look malformed or excessively large — that is normal and expected. Always verify rendering and SEO output against a production build before drawing conclusions about page size or HTML structure.
Sitemaps
owlCMS is a headless API — your frontend is responsible for generating sitemap.xml. This section shows how to do it correctly at any scale.
1. Paginate through all posts
The /v1/posts endpoint returns up to 100 posts per request. If you have more, loop with offset until posts.length === pagination.total. Each post includes updatedAt — use it as <lastmod> so Google knows what changed.
// Fetch every published post using pagination
async function getAllPosts(apiKey) {
const posts = [];
const limit = 100;
let offset = 0;
while (true) {
const res = await fetch(
`https://owlcms.com/v1/posts?limit=${limit}&offset=${offset}`,
{ headers: { 'X-API-Key': apiKey } }
);
const { data, pagination } = await res.json();
posts.push(...data);
if (posts.length >= pagination.total) break;
offset += limit;
}
return posts; // array of all published posts
}2. Generate the XML — Astro
Create a dynamic route that fetches all posts at build time (SSG) or on each request (SSR) and streams back valid XML.
// Astro: src/pages/sitemap.xml.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => {
const posts = await getAllPosts(import.meta.env.OWL_API_KEY);
const urls = posts.map((post) => `
<url>
<loc>https://yoursite.com/blog/${post.slug}</loc>
<lastmod>${new Date(post.updatedAt).toISOString()}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>`).join('');
return new Response(
`<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`,
{ headers: { 'Content-Type': 'application/xml' } }
);
};2. Generate the XML — Next.js App Router
Next.js 13+ has a built-in sitemap.ts convention that handles the XML formatting for you.
// Next.js App Router: app/sitemap.ts
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts(process.env.OWL_API_KEY!);
return posts.map((post) => ({
url: `https://yoursite.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly',
priority: 0.7,
}));
}3. Thousands of posts — use a sitemap index
Google's limit is 50,000 URLs per sitemap file. Once you exceed ~5,000 posts, split into multiple sitemaps and reference them from a sitemap-index.xml.
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://yoursite.com/sitemap-posts-1.xml</loc>
<lastmod>2025-01-15T10:00:00Z</lastmod>
</sitemap>
<sitemap>
<loc>https://yoursite.com/sitemap-posts-2.xml</loc>
<lastmod>2025-01-15T10:00:00Z</lastmod>
</sitemap>
</sitemapindex>4. Keep the sitemap fresh automatically
For SSG sites the sitemap is stale the moment you publish a new post. Set a Webhook URL in the Pages dashboard and point it at your deployment provider's deploy hook. Every time owlCMS publishes a post the webhook fires, your build triggers, and your sitemap regenerates automatically. See the Webhooks section for setup instructions.
Don't forget robots.txt
Point Google to your sitemap by adding Sitemap: https://yoursite.com/sitemap.xml to your robots.txt. Then submit the URL in Google Search Console → Sitemaps for faster indexing.
Authentication
All API requests require an API key. Create and manage keys from your Dashboard Settings.
Recommended
X-API-Key header
X-API-Key: owlcms_your_api_key_hereAlternative
Bearer token
Authorization: Bearer owlcms_your_api_key_hereKeep your API keys secure
Never expose keys in client-side code or public repositories. Always use environment variables on your server or build process.
Endpoints
/v1/postsRetrieve a paginated list of all published posts. Featured posts are always returned first, followed by the rest ordered by most recently updated. Each post includes a computed excerpt (plain-text preview, ≤160 chars) and coverImage field.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | string | — | Filter posts by page slug (e.g. my-blog). Returns only posts assigned to that page. |
| limit | integer | 20 | Number of posts to return (max: 100). |
| offset | integer | 0 | Number of posts to skip. |
Example request
curl -X GET "https://owlcms.com/v1/posts" \
-H "X-API-Key: owlcms_your_api_key_here"
# Filter by page slug
curl -X GET "https://owlcms.com/v1/posts?page=my-blog" \
-H "X-API-Key: owlcms_your_api_key_here"Example response
{
"data": [
{
"_id": "64f8a1b2c3d4e5f6a7b8c9d0",
"title": "Hello World",
"slug": "hello-world",
"content": { "blocks": [...] },
"authorName": "John Doe",
"featured": true,
"excerpt": "A short plain-text preview of the post...",
"coverImage": "https://cdn.owlcms.io/images/hello-world.jpg",
"createdAt": "2025-01-15T10:30:00.000Z",
"updatedAt": "2025-01-15T10:30:00.000Z"
}
],
"pagination": { "limit": 20, "offset": 0, "total": 42 }
}/v1/posts/:idRetrieve a single post by its unique MongoDB ObjectId.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| id | string | — | 24-character MongoDB ObjectId. |
Example response
{
"data": {
"_id": "64f8a1b2c3d4e5f6a7b8c9d0",
"title": "Hello World",
"slug": "hello-world",
"content": { "blocks": [...] },
"authorName": "John Doe",
"seoKeywords": ["hello", "world"],
"createdAt": "2025-01-15T10:30:00.000Z",
"updatedAt": "2025-01-15T10:30:00.000Z"
}
}/v1/posts/slug/:slugRetrieve a single post by its URL slug. Ideal for building frontend routes like /blog/:slug.
Example request
curl -X GET "https://owlcms.com/v1/posts/slug/my-post-slug" \
-H "X-API-Key: owlcms_your_api_key_here"JavaScript
const response = await fetch('https://owlcms.com/v1/posts/slug/my-post', {
headers: {
'X-API-Key': 'owlcms_your_api_key_here'
}
});
const { data: post } = await response.json();
console.log(post.title, post.content);/v1/posts/render/:slugReturns a post with a pre-rendered html field. Perfect for quick integrations without needing a content parser.
Example
const response = await fetch('https://owlcms.com/v1/posts/render/my-post', {
headers: {
'X-API-Key': 'owlcms_your_api_key_here'
}
});
const { data: post } = await response.json();
// 'post.html' contains a pre-rendered HTML string
document.getElementById('content').innerHTML = post.html;Featured Posts
Any post can be marked as featured by clicking the star icon in the Posts dashboard. A filled orange star means the post is featured.
GET /v1/posts — no extra filtering needed. Use the dedicated /v1/posts/featured endpoint when you only want featured posts (e.g. a hero carousel or "Editor's picks" section)./v1/posts/featuredReturns all published posts that have been starred as featured. Supports the same page, limit, and offset query parameters as the main list endpoint.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | string | — | Filter featured posts by page slug. |
| limit | integer | 20 | Number of posts to return (max: 100). |
| offset | integer | 0 | Number of posts to skip. |
Example request
curl -X GET "https://owlcms.com/v1/posts/featured" \
-H "X-API-Key: owlcms_your_api_key_here"
# Filter by page slug
curl -X GET "https://owlcms.com/v1/posts/featured?page=my-blog" \
-H "X-API-Key: owlcms_your_api_key_here"The response shape is identical to GET /v1/posts — an array of post objects under data with a pagination block. Each post includes the computed excerpt and coverImage fields.
Webhooks & Deployment
Webhooks allow owlCMS to notify your website whenever content changes. Essential for triggering automatic rebuilds of static sites. Webhooks are configured per page in the Pages dashboard.
Payload format
When an event occurs, owlCMS sends a POST request to your configured URL:
{
"event": "post.created",
"timestamp": "2025-01-15T10:30:00.000Z",
"data": {
"id": "64f8a1b2c3d4e5f6a7b8c9d0",
"title": "My New Post",
"slug": "my-new-post"
}
}Verifying signatures
If you set a webhook secret on a page, owlCMS signs every payload with HMAC-SHA256 and includes the signature in the X-Webhook-Signature header:
X-Webhook-Signature: sha256=<hex-digest>
// Verify in Node.js
import { createHmac } from 'crypto';
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
if (req.headers['x-webhook-signature'] !== expected) throw new Error('Invalid signature');Automatic rebuilds — deploy hook setup
Generate a deploy hook URL from your hosting provider, then paste it into the Webhook URL field on the relevant page. Every time owlCMS publishes a post it will call that URL and trigger a rebuild.
Vercel — Settings → Git → Deploy Hooks
Netlify — Site Settings → Build & Deploy → Build hooks
Cloudflare Pages — Settings → Builds & deployments → Deploy hooks
Error Codes
Standard HTTP status codes indicate success or failure.
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 401 | Unauthorized | Missing or invalid API key |
| 404 | Not Found | Requested resource doesn't exist |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Internal error on our end |
401 Unauthorized
{ "error": "Unauthorized", "message": "Valid API key required" }404 Not Found
{ "error": "Not Found", "message": "Post not found" }Quick Start
Get your first API response in under two minutes.
Get your API key
Log in to your Dashboard, go to Settings, and create a new key in the API Keys section.
Fetch all posts
Call the list endpoint to retrieve your published posts.
// All posts
const response = await fetch('https://owlcms.com/v1/posts', {
headers: { 'X-API-Key': 'owlcms_your_api_key_here' }
});
// Filter by page slug
const response = await fetch('https://owlcms.com/v1/posts?page=my-blog', {
headers: { 'X-API-Key': 'owlcms_your_api_key_here' }
});
const data = await response.json();
console.log(data.data); // Array of postsRender a single post
Use the slug endpoint to retrieve and display individual posts in your frontend.
const response = await fetch('https://owlcms.com/v1/posts/render/my-post', {
headers: {
'X-API-Key': 'owlcms_your_api_key_here'
}
});
const { data: post } = await response.json();
// 'post.html' contains a pre-rendered HTML string
document.getElementById('content').innerHTML = post.html;Here's a prompt for helping with the implementation, if you are building with AI
Copy the prompt below into any AI coding assistant (Claude, Copilot, Cursor, etc.). Replace [framework] and <MY_KEY> with your values.
Implement a blog using the owlCMS headless CMS API in my [framework] project.
API base URL: https://owlcms.com/v1
Auth: X-API-Key: <MY_KEY> header on every request (store in an environment variable)
Endpoints:
GET /v1/posts list of published posts (?limit, ?offset, ?page)
GET /v1/posts/slug/:slug single post by slug
GET /v1/posts/render/:slug single post with pre-rendered html field
GET /v1/posts/featured featured posts only
Post fields: _id, title, slug, excerpt, coverImage, authorName, featured, createdAt, updatedAt
Requirements:
- Always fetch on the server (SSR or SSG) — never from the client
- /blog listing page showing all posts with title, excerpt, and cover image
- /blog/[slug] detail page rendering the html field from /v1/posts/render/:slug
- Correct <title>, meta description, og tags, and canonical URL on every post page
- sitemap.xml listing all published post URLs using the updatedAt field as lastmodNeed help?
Contact our support team at [email protected] or check the community forum for integration examples.