Content API
Fetch your published blog posts from IndieBob and display them on any website. REST API with JSON responses, RSS/JSON feeds, and XML sitemaps.
Overview
The Content API gives you programmatic access to your published blog posts. Use it to build a blog on your own domain, populate your Next.js or Astro site, or syndicate content anywhere.
Authentication
All requests require a Content API key passed as a Bearer token. Create keys in your project's Settings page.
Keys use the ib_pk_ prefix. They are hashed before storage — the raw key is shown only once on creation.
Revoking a key is immediate and permanent.
Rate Limits
| Limit | 100 requests / minute / key |
| Exceeded | 429 Too Many Requests |
Endpoints
/api/content/v1/postsReturns a paginated list of published posts.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
| page | number | 1 | Page number |
| limit | number | 20 | Items per page (max 100) |
| sort | string | publishedAt | publishedAt, updatedAt, or title |
| order | string | desc | asc or desc |
Response
{
"data": [
{
"id": "uuid",
"slug": "my-first-post",
"title": "My First Post",
"excerpt": "A short description...",
"metaTitle": "My First Post | Blog",
"metaDescription": "...",
"ogImageUrl": "/api/og/proj/postId",
"publishedAt": "2026-03-01T12:00:00.000Z",
"updatedAt": "2026-03-01T12:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 42,
"totalPages": 3
}
}/api/content/v1/posts/:slugReturns a single published post by slug, including full content.
Response
{
"id": "uuid",
"slug": "my-first-post",
"title": "My First Post",
"bodyHtml": "<h2>Introduction</h2><p>...</p>",
"bodyMarkdown": "## Introduction\n\n...",
"excerpt": "...",
"metaTitle": "My First Post | Blog",
"metaDescription": "...",
"ogImageUrl": "/api/og/proj/postId",
"seoScore": 82,
"publishedAt": "2026-03-01T12:00:00.000Z",
"updatedAt": "2026-03-01T12:00:00.000Z"
}Returns 404 if the slug doesn't exist or the post isn't published.
/api/content/v1/feed.jsonJSON Feed 1.1 — 50 most recent posts. Content-Type: application/feed+json.
/api/content/v1/feed.xml RSS 2.0 feed — 50 most recent posts. Content-Type: application/rss+xml.
/api/content/v1/sitemap.xml XML sitemap of all published posts. Content-Type: application/xml. Cached for 1 hour.
Webhooks
Webhooks notify your server in real-time when content changes. Configure them in your project's Settings page.
Events
| Event | Trigger |
|---|---|
| content.published | A post is published |
| content.updated | A published post is edited |
| content.unpublished | A post is reverted to draft |
Payload Format
{
"event": "content.published",
"timestamp": "2026-03-01T12:00:00.000Z",
"project": { "id": "uuid", "name": "My Project" },
"data": {
"post": {
"id": "uuid",
"slug": "my-first-post",
"title": "My First Post",
"excerpt": "...",
"ogImageUrl": "/api/og/proj/postId",
"publishedAt": "2026-03-01T12:00:00.000Z"
}
}
}Headers included with every delivery:
| X-IndieBob-Signature | HMAC-SHA256 signature |
| X-IndieBob-Event | Event type |
| X-IndieBob-Delivery | Unique delivery UUID |
Signature Verification
Verify the X-IndieBob-Signature header to ensure the payload was sent by IndieBob. The signature is computed as sha256=HMAC-SHA256(body, secret).
// Node.js verification
import { createHmac } from 'crypto'
function verifySignature(body: string, signature: string, secret: string) {
const expected = 'sha256=' + createHmac('sha256', secret)
.update(body, 'utf8')
.digest('hex')
return signature === expected
}Webhooks retry up to 3 times (0s, 1min, 5min) on failure. After 10 consecutive failures, the webhook is auto-disabled.
Caching
All responses include caching headers. Use conditional requests to minimize bandwidth.
| Cache-Control | public, max-age=60, stale-while-revalidate=300 |
| ETag | Content hash — send back in If-None-Match |
| Last-Modified | Send back in If-Modified-Since for 304 |
Integration Examples
Next.js
// app/blog/page.tsx
const API_KEY = process.env.INDIEBOB_API_KEY
async function getPosts() {
const res = await fetch('https://indiebob.com/api/content/v1/posts', {
headers: { Authorization: `Bearer ${API_KEY}` },
next: { revalidate: 60 },
})
return res.json()
}
export default async function BlogPage() {
const { data: posts } = await getPosts()
return (
<ul>
{posts.map(post => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
)
}Astro
--- // src/pages/blog/[slug].astro const { slug } = Astro.params const res = await fetch( `https://indiebob.com/api/content/v1/posts/${slug}`, { headers: { Authorization: `Bearer ${globalThis._importMeta_.env.INDIEBOB_API_KEY}` } } ) const post = await res.json() --- <article> <h1>{post.title}</h1> <Fragment set:html={post.bodyHtml} /> </article>
Generic Fetch
const response = await fetch(
'https://indiebob.com/api/content/v1/posts?limit=10',
{ headers: { Authorization: 'Bearer ib_pk_your_key' } }
)
const { data, pagination } = await response.json()
console.log(`${pagination.total} posts, showing page ${pagination.page}`)
data.forEach(post => console.log(post.title))