API Reference

Get API key
REST API · v1

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/v1

Best Practices & SEO

Choosing the right rendering strategy is critical for performance and search visibility.

SSG — Static Site Generation

Recommended

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

Pro tip: Use SSG + Webhooks — save a post in owlCMS → webhook triggers a rebuild → your site becomes lightning-fast static files.

SSR — Server-Side Rendering

Dynamic

Used 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 SEO

Used 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_here

Alternative

Bearer token

Authorization: Bearer owlcms_your_api_key_here

Keep 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

GET/v1/posts

Retrieve 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

ParameterTypeDefaultDescription
pagestringFilter posts by page slug (e.g. my-blog). Returns only posts assigned to that page.
limitinteger20Number of posts to return (max: 100).
offsetinteger0Number 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 }
}
GET/v1/posts/:id

Retrieve a single post by its unique MongoDB ObjectId.

Query parameters

ParameterTypeDefaultDescription
idstring24-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"
  }
}
GET/v1/posts/slug/:slug

Retrieve 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);
GET/v1/posts/render/:slug

Returns 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;

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.

CodeStatusDescription
200OKRequest succeeded
401UnauthorizedMissing or invalid API key
404Not FoundRequested resource doesn't exist
429Too Many RequestsRate limit exceeded
500Server ErrorInternal 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.

1

Get your API key

Log in to your Dashboard, go to Settings, and create a new key in the API Keys section.

2

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 posts
3

Render 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;
4

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 lastmod

Need help?

Contact our support team at [email protected] or check the community forum for integration examples.