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.

Looking to drive IndieBob rather than read from it — publish posts, run outreach, ask what to do next? That is the agent API, with an MCP server and a machine-readable capability document at /agents.md.

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 — one key on the free plan, more on Plus.

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",
      "writtenWithCredit": true,
      "creditUrl": "https://www.indiebob.com/?ref=post-credit"
    }
  ],
  "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",
  "writtenWithCredit": true,
  "creditUrl": "https://www.indiebob.com/?ref=post-credit"
}

Returns 404 if the slug doesn't exist or the post isn't published.

The post credit

When writtenWithCredit is true, bodyHtml ends with a one-line footer linking to creditUrl. The block is marked data-indiebob-credit="1" so you can remove it and render your own — nothing is hidden in markup you don't control. The project owner turns it off in project settings → Post Credit; the change applies on the next publish.

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.updatedAn already-published post is published again. Saving an edit sends nothing.
content.unpublishedA post is reverted to draft. data.post carries id, slug and title only.

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",
      "writtenWithCredit": true,
      "creditUrl": "https://www.indiebob.com/?ref=post-credit"
    }
  }
}

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(rawBody, secret) — over the exact bytes we sent, so verify before parsing. A re-serialised body will never match.

// Node.js verification
import { createHmac, timingSafeEqual } from 'node:crypto'

function verifySignature(rawBody: string, signature: string | undefined, secret: string) {
  if (!signature) return false
  const expected = Buffer.from(
    'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex'),
  )
  const received = Buffer.from(signature)
  // Constant-time: a plain === leaks how much of a guess was right.
  if (expected.length !== received.length) return false
  return timingSafeEqual(expected, received)
}

Deliveries retry up to 3 times (immediately, +1min, +5min) on 5xx, 429 or no response, reusing the same X-IndieBob-Delivery id — make your handler idempotent. Any other 4xx is treated as a deliberate refusal and is not retried. Each attempt times out after 10s. 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>

The whole site — Astro, hosted free on DigitalOcean, the blog built from this API at build time, RSS from the same posts — is walked step by step in the Astro on DigitalOcean guide.

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))