API Reference

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.

Base URL:https://indiebob.com/api/content/v1

Authentication

All requests require a Content API key passed as a Bearer token. Create keys in your project's Settings page.

curl https://indiebob.com/api/content/v1/posts \
-H "Authorization: Bearer ib_pk_your_key_here"

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

Limit100 requests / minute / key
Exceeded429 Too Many Requests

Endpoints

GET/api/content/v1/posts

Returns a paginated list of published posts.

Query Parameters

ParamTypeDefaultDescription
pagenumber1Page number
limitnumber20Items per page (max 100)
sortstringpublishedAtpublishedAt, updatedAt, or title
orderstringdescasc 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
  }
}
GET/api/content/v1/posts/:slug

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

GET/api/content/v1/feed.json

JSON Feed 1.1 — 50 most recent posts. Content-Type: application/feed+json.

GET/api/content/v1/feed.xml

RSS 2.0 feed — 50 most recent posts. Content-Type: application/rss+xml.

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

EventTrigger
content.publishedA post is published
content.updatedA published post is edited
content.unpublishedA 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-SignatureHMAC-SHA256 signature
X-IndieBob-EventEvent type
X-IndieBob-DeliveryUnique 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-Controlpublic, max-age=60, stale-while-revalidate=300
ETagContent hash — send back in If-None-Match
Last-ModifiedSend 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))