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://api.owlcms.io/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.
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 already includes updatedAt — use it as <lastmod> so Google knows which pages changed since the last crawl.
// 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://api.owlcms.io/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, Nuxt, and SvelteKit all support this pattern natively.
// 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 (and 50 MB uncompressed). Once you exceed ~5,000 posts we recommend splitting into multiple sitemaps and referencing them from a sitemap-index.xml. Generate each child sitemap with a fixed offset window (e.g. 0–999, 1000–1999, …).
<?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> <!-- slugs 0–999 -->
<lastmod>2025-01-15T10:00:00Z</lastmod>
</sitemap>
<sitemap>
<loc>https://yoursite.com/sitemap-posts-2.xml</loc> <!-- slugs 1000–1999 -->
<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. The fix is simple: set a Webhook URL on your page (in the Pages dashboard) and point it at your deployment provider's deploy hook. Every time owlCMS creates a post the webhook fires, your build triggers, and your sitemap regenerates automatically with the latest updatedAt timestamps. See the Webhooks section for deploy hook 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. Each post includes a computed excerpt (plain-text preview, ≤160 chars) and coverImage field.
/v1/posts/:idRetrieve a single post by its unique MongoDB ObjectId.
/v1/posts/slug/:slugRetrieve a single post by its URL slug. Ideal for building frontend routes like /blog/:slug.
/v1/posts/render/:slugReturns a post with a pre-rendered html field. Perfect for quick integrations without needing a content parser.
Featured Posts
Any post can be marked as featured by clicking the star icon in the Posts dashboard. Featured posts are returned by a dedicated endpoint so you can highlight them in hero sections, carousels, or sidebars without extra client-side filtering.
/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.
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 — each page can have its own webhook URL.
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 in the Pages dashboard. 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://api.owlcms.io/v1/posts', {
headers: { 'X-API-Key': 'owlcms_your_api_key_here' }
});
// Filter by page slug
const response = await fetch('https://api.owlcms.io/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://api.owlcms.io/v1/posts/render/my-post', {
headers: {
'X-API-Key': 'owlcms_your_api_key_here'
}
});
const { data: post } = await response.json();
// 'post.html' now contains a pre-rendered HTML string
document.getElementById('content').innerHTML = post.html;Need help?
Contact our support team or check the community forum for integration examples.