Complete reference for the SimplyStack Content API — list, create, update, delete, and build navigation from structured content.
Content API
The Content API provides full CRUD endpoints for managing structured content in your SimplyStack project — blog posts, documentation pages, or any other content type you define. All operations require a project-specific API key.
Authentication
Include your project API key in the x-api-key header on every request. Personal tokens are not accepted — only project-specific keys work with the Content API. Get yours from Settings → API Keys in the dashboard.
curl -H "x-api-key: your-project-api-key" \
"https://www.simplystack.dev/api/v1/content?type=blog"Your API key is scoped to one project — all content reads and writes are automatically filtered to that project.
Endpoints Overview
- GET /api/v1/content — list content items
- POST /api/v1/content — create a new content item
- PUT /api/v1/content — update by ID in request body
- DELETE /api/v1/content?id={uuid} — delete by UUID
- GET /api/v1/content/{slug} — fetch single item by slug or UUID
- PUT /api/v1/content/{slug} — update single item by slug or UUID
- DELETE /api/v1/content/{slug} — delete single item by slug or UUID
- GET /api/v1/content/hierarchy — navigation tree for a content type
1. List Content Items
GET /api/v1/content
Returns a paginated list of published content items. The list response does not include the content body — use include_content=true on the single-item endpoint when you need the full Editor.js blocks.
Query Parameters
- type (string) — content type, e.g. blog, docs, or any custom string
- status (string) — default: published. Options: published, draft
- category (string) — filter by category label
- tags (string) — comma-separated tag values, e.g. javascript,react
- query (string) — full-text search across title and excerpt
- parent_id (UUID) — filter by parent content item
- page (number) — page number, 1-indexed. Default: 1
- limit (number) — items per page, max 100. Default: 20
- sort_by (string) — field to sort by. Default: published_at. Also accepts: title, created_at, sort_order, sidebar_position
- sort_order (string) — asc or desc. Default: desc
Example
// Get first 10 blog posts
const res = await fetch(
"https://www.simplystack.dev/api/v1/content?type=blog&limit=10",
{ headers: { "x-api-key": "your-project-api-key" } }
);
const { data, pagination } = await res.json();
console.log(data); // array of content items (no content body)
console.log(pagination); // { page, limit, total, totalPages, hasNextPage, hasPrevPage }List Response Shape
{
"data": [
{
"id": "9629d631-59e0-419f-a018-98c79e8236c7",
"title": "Getting Started",
"slug": "getting-started",
"excerpt": "...",
"type": "docs",
"category": "Getting Started",
"status": "published",
"tags": [],
"parent_id": null,
"sort_order": 0,
"nav_title": null,
"sidebar_position": 0,
"hide_from_nav": false,
"meta_title": "...",
"meta_description": "...",
"meta_keywords": [],
"canonical_url": null,
"published_at": "2025-10-13T01:53:11.3+00:00",
"created_at": "2025-10-13T01:53:11.360111+00:00",
"updated_at": "2025-10-13T17:05:23.107246+00:00"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 9,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
}2. Get Single Content Item
GET /api/v1/content/{identifier}
Fetch one content item by its slug or UUID. The identifier type is detected automatically — pass either format in the URL path.
Query Parameters
- include_content (boolean) — set to true to include the full Editor.js content blocks. Default: false
- type (string) — optional filter to restrict match to a specific content type
Example
// Fetch by slug with full content body
const res = await fetch(
"https://www.simplystack.dev/api/v1/content/getting-started?include_content=true",
{ headers: { "x-api-key": "your-project-api-key" } }
);
const { data } = await res.json();
// data.content contains the Editor.js blocks
console.log(data.content.blocks);
// Also works by UUID
const res2 = await fetch(
"https://www.simplystack.dev/api/v1/content/9629d631-59e0-419f-a018-98c79e8236c7",
{ headers: { "x-api-key": "your-project-api-key" } }
);Single Item Response (with include_content=true)
Returns all fields including the content object. Without include_content=true, the content, content_html, content_format, featured_image_id, scheduled_for, auto_publish, and ai_generated fields are omitted.
{
"data": {
"id": "9629d631-...",
"title": "Getting Started",
"slug": "getting-started",
"content": {
"time": 1728765600000,
"version": "2.28.2",
"blocks": [
{ "type": "header", "data": { "text": "Getting Started", "level": 1 } },
{ "type": "paragraph", "data": { "text": "Welcome..." } }
]
},
"content_html": null,
"content_format": "json",
"excerpt": "...",
"type": "docs",
"category": "Getting Started",
"status": "published",
"tags": [],
"parent_id": null,
"sidebar_position": 0,
"hide_from_nav": false,
"meta_title": "...",
"meta_description": "...",
"meta_keywords": [],
"canonical_url": "",
"redirect_from": [],
"featured_image_id": null,
"published_at": "2025-10-13T01:53:11.3+00:00",
"project_id": "8cec3973-...",
"user_id": null,
"created_at": "...",
"updated_at": "..."
}
}3. Create Content
POST /api/v1/content
Creates a new content item. Returns the full created item with HTTP 201. Both title and content are required — all other fields are optional.
Request Body Fields
- title (string, required) — content title
- content (object, required) — Editor.js JSON with a blocks array
- slug (string) — URL slug, auto-generated from title if omitted. Must be unique within the project
- type (string) — default: blog. Use any string: blog, docs, guide, etc.
- status (string) — default: draft. Use published to make it live immediately
- excerpt (string) — short summary shown in listings
- category (string) — grouping label for navigation sections
- tags (string[]) — array of tag strings
- parent_id (UUID) — UUID of a parent item for nested content
- sort_order (number) — manual ordering within a group. Default: 0
- nav_title (string) — short display title for navigation menus
- sidebar_position (number) — numeric position in sidebar navigation
- hide_from_nav (boolean) — exclude from hierarchy endpoint. Default: false
- meta_title (string) — SEO title tag
- meta_description (string) — SEO meta description
- meta_keywords (string[]) — SEO keywords array
- canonical_url (string) — canonical URL for SEO
Setting status to "published" automatically sets published_at to the current timestamp. If slug is omitted, it is generated from the title by lowercasing and replacing spaces with hyphens.
Example
const res = await fetch("https://www.simplystack.dev/api/v1/content", {
method: "POST",
headers: {
"x-api-key": "your-project-api-key",
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "My First Blog Post",
type: "blog",
status: "published",
excerpt: "An introduction to SimplyStack.",
category: "Tutorials",
tags: ["getting-started"],
content: {
time: Date.now(),
version: "2.28.2",
blocks: [
{
type: "header",
data: { text: "Hello World", level: 1 }
},
{
type: "paragraph",
data: { text: "This is my first post." }
}
]
}
})
});
const { data } = await res.json(); // 201 Created
console.log(data.id, data.slug); // UUID + auto-generated slug4. Update Content
There are two ways to update a content item. Send only the fields you want to change — unmentioned fields are left untouched.
Option A — by slug or UUID in the URL path
PUT /api/v1/content/{slug-or-uuid}
// Update by slug
await fetch("https://www.simplystack.dev/api/v1/content/my-blog-post", {
method: "PUT",
headers: {
"x-api-key": "your-project-api-key",
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "Updated Title",
tags: ["updated", "blog"],
meta_description: "New SEO description"
})
});
// Update by UUID — same endpoint, same body
await fetch("https://www.simplystack.dev/api/v1/content/9871c242-e7bc-4723-a19b-0831288d8842", {
method: "PUT",
headers: { "x-api-key": "your-project-api-key", "Content-Type": "application/json" },
body: JSON.stringify({ status: "published" })
});Option B — by ID in the request body
PUT /api/v1/content
await fetch("https://www.simplystack.dev/api/v1/content", {
method: "PUT",
headers: {
"x-api-key": "your-project-api-key",
"Content-Type": "application/json"
},
body: JSON.stringify({
id: "9871c242-e7bc-4723-a19b-0831288d8842", // required
title: "Updated Title",
status: "published"
})
});Both options return the full updated item in a { data: {...} } envelope.
5. Delete Content
There are two ways to delete a content item.
Option A — by slug or UUID in the URL path
DELETE /api/v1/content/{slug-or-uuid}
// Delete by slug
const res = await fetch(
"https://www.simplystack.dev/api/v1/content/my-blog-post",
{ method: "DELETE", headers: { "x-api-key": "your-project-api-key" } }
);
// Response: { message: "Content deleted successfully", deleted: { id, title, slug } }Option B — by UUID query parameter
DELETE /api/v1/content?id={uuid}
await fetch(
"https://www.simplystack.dev/api/v1/content?id=9871c242-e7bc-4723-a19b-0831288d8842",
{ method: "DELETE", headers: { "x-api-key": "your-project-api-key" } }
);
// Response: { message: "Content deleted successfully" }6. Content Hierarchy
GET /api/v1/content/hierarchy
Returns all published, nav-visible content of a given type as a tree structure sorted by sidebar_position. Items that have a parent_id appear nested under their parent in a children array. Items with hide_from_nav: true are excluded.
Query Parameters
- type (string, required) — content type to build hierarchy for, e.g. docs
- parent_id (UUID) — start tree from a specific parent node
- max_depth (number) — limit nesting depth. 0 means unlimited
Example
const res = await fetch(
"https://www.simplystack.dev/api/v1/content/hierarchy?type=docs",
{ headers: { "x-api-key": "your-project-api-key" } }
);
const { data } = await res.json();
// data is an array of top-level items, each with a children array
data.forEach(item => {
console.log(item.slug, item.sidebar_position, item.children);
});{
"data": [
{
"id": "9629d631-...",
"title": "Getting Started",
"slug": "getting-started",
"nav_title": "",
"parent_id": null,
"sort_order": 0,
"sidebar_position": 0,
"hide_from_nav": false,
"type": "docs",
"children": []
},
{
"id": "7a23c921-...",
"title": "API Overview",
"slug": "api-overview",
"sidebar_position": 1,
"children": [
{
"id": "child-uuid",
"title": "Authentication",
"slug": "authentication",
"parent_id": "7a23c921-...",
"sidebar_position": 1,
"children": []
}
]
}
]
}Editor.js Content Structure
Content is stored as Editor.js JSON. When you POST or PUT content, the content field must be a valid Editor.js object with a blocks array. When you fetch with include_content=true, you get the same structure back.
{
"time": 1728765600000,
"version": "2.28.2",
"blocks": [
{ "id": "optional-id", "type": "header", "data": { "text": "Title", "level": 1 } },
{ "type": "paragraph", "data": { "text": "Body text here." } }
]
}Supported Block Types
- header — heading. data: { text, level } where level is 1–6
- paragraph — body text. data: { text }
- list — ordered or unordered list. data: { items: string[], style: "ordered" | "unordered" }
- code — code block. data: { code, language }
- quote — block quote. data: { text, caption }
Converting Blocks to HTML
function renderBlock(block) {
switch (block.type) {
case "header": {
const tag = `h${block.data.level}`;
return `<${tag}>${block.data.text}</${tag}>`;
}
case "paragraph":
return `<p>${block.data.text}</p>`;
case "list": {
const tag = block.data.style === "ordered" ? "ol" : "ul";
const items = block.data.items.map(item => `<li>${item}</li>`).join("");
return `<${tag}>${items}</${tag}>`;
}
case "code":
return `<pre><code>${block.data.code}</code></pre>`;
case "quote":
return `<blockquote>${block.data.text}</blockquote>`;
default:
return "";
}
}
function renderContent(content) {
return content.blocks.map(renderBlock).join("\n");
}Error Reference
All errors return JSON with an error field and a standard HTTP status code:
{ "error": "Content not found" }Error Codes
- 400 "Title is required" — POST body missing title
- 400 "Content is required" — POST body missing content
- 400 "Content ID is required" — PUT or DELETE on root endpoint without id
- 400 "type parameter is required" — hierarchy endpoint called without type
- 401 "Missing API key" — no x-api-key header
- 401 "Invalid API key" — key not found or revoked
- 403 "Content operations require a project-specific API key" — personal token used instead of project key
- 404 "Content not found" — slug or UUID does not exist in this project
- 409 "Content with slug '...' already exists" — duplicate slug on POST
- 500 "Internal server error" — server-side error
Field Reference
Every field returned by the API:
- id (UUID) — unique identifier
- title (string) — content title
- slug (string) — URL-safe identifier, unique per project
- type (string) — content type: blog, docs, or any custom string
- status (string) — draft, published, archived, or scheduled
- excerpt (string) — short summary shown in listings
- category (string) — grouping label used for navigation sections
- tags (string[]) — array of tag strings
- parent_id (UUID | null) — parent item UUID for nested content
- sort_order (number) — manual sort position within a group
- nav_title (string) — short title for navigation menus
- sidebar_position (number) — position in sidebar navigation
- hide_from_nav (boolean) — if true, excluded from hierarchy endpoint
- meta_title (string) — SEO tag
- meta_description (string) — SEO meta description
- meta_keywords (string[]) — SEO keywords
- canonical_url (string) — canonical URL for SEO
- redirect_from (string[]) — legacy paths that redirect here
- featured_image_id (UUID | null) — reference to a Storage asset
- published_at (ISO 8601) — when the item was published
- scheduled_for (ISO 8601 | null) — future publish date
- auto_publish (boolean) — auto-publish on scheduled_for date
- ai_generated (boolean) — whether the content was AI-generated
- content (object) — Editor.js JSON. Only present with include_content=true
- content_html (string | null) — pre-rendered HTML if set
- content_format (string) — always json for Editor.js content
- project_id (UUID) — project this item belongs to
- user_id (UUID | null) — author user ID
- created_at (ISO 8601) — creation timestamp
- updated_at (ISO 8601) — last update timestamp
Next Steps
- Storage API — upload and serve images and files
- Projects API — manage projects programmatically
- SDK Documentation — TypeScript SDK for easier integration
- Code Examples — real-world patterns for blogs and docs sites