Blog
List posts
GET /api/v1/blog/posts
Metadata only — no MDX bodies, so it's cheap enough to render an index from one call. Public; a key scopes and rate-limits the request.
| Parameter | Default | Notes |
|---|---|---|
projectId | — | Required unless a key is sent |
tag | — | Exact-match filter on a single tag |
limit | 20 | Clamped to 1–100 |
offset | 0 | Offset pagination — see Pagination |
curl "https://api.inkform.dev/api/v1/blog/posts?projectId=YOUR_PROJECT_ID&tag=engineering&limit=10"{
"data": [
{
"slug": "hello-world",
"title": "Hello World",
"date": "2026-09-05",
"author": "Charan",
"tags": ["engineering"],
"series": null,
"description": "Why we built this.",
"coverImage": "https://example.com/cover.png",
"excerpt": "Our first post.",
"readTimeMinutes": 4
}
],
"pageInfo": { "hasNextPage": true, "endCursor": null },
"totalCount": 37
}totalCount is the count after the tag filter, so it's the right number for
"showing 10 of 37".
Get one post
GET /api/v1/blog/posts/{slug}
Same fields plus body — the MDX with frontmatter stripped.
curl "https://api.inkform.dev/api/v1/blog/posts/hello-world?projectId=YOUR_PROJECT_ID"{
"data": {
"slug": "hello-world",
"title": "Hello World",
"body": "Our first post.\n\n## Why\n\nBecause..."
}
}Returns 404 for an unknown slug, and for a slug that exists but isn't published —
drafts are never served here.
Create or update a post
POST /api/v1/blog/posts
Requires a secret key with content:write. Writes a commit to your connected repo; the
response carries the commit URL. Posting an existing slug updates that post.
{
"frontmatter": {
"title": "Hello World",
"slug": "hello-world",
"date": "2026-09-05",
"author": "Charan",
"tags": ["engineering"],
"status": "published",
"series": null,
"description": "Why we built this.",
"coverImage": null,
"ogImage": null,
"ogTitle": null
},
"doc": {
"blocks": [
{ "id": "1", "type": "raw", "mdx": "## Why\n\nBecause the old way hurt." }
]
}
}Only title is required. slug is derived from the title if omitted, date defaults to
today, and status defaults to published.
The raw block
doc.blocks is the structured block model the editor uses. If you just have markdown,
the raw block is the documented escape hatch — one block, your MDX, done:
{ "id": "1", "type": "raw", "mdx": "# Anything\n\nPlain markdown or MDX." }Structured block types (paragraph, heading, list, code, and others) exist for
callers building an editor UI. For programmatic publishing, raw is almost always what
you want.
curl -X POST "https://api.inkform.dev/api/v1/blog/posts" \
-H "Authorization: Bearer inkform_sk_your_key" \
-H "Content-Type: application/json" \
-d '{
"frontmatter": { "title": "Shipped: faster builds" },
"doc": { "blocks": [{ "id": "1", "type": "raw", "mdx": "Builds are 40% faster." }] }
}'{
"data": {
"slug": "shipped-faster-builds",
"fileName": "shipped-faster-builds.mdx",
"commitUrl": "https://github.com/you/repo/commit/abc123"
}
}A write invalidates the read cache immediately, so the post is fetchable on the next request rather than after a TTL.
Write errors
| Status | Cause |
|---|---|
400 | Body failed validation — the message names the offending fields |
403 | Key lacks content:write |
404 | No blog project for this key |
409 | Project has no connected GitHub repository |