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.

Bash
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

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

JSON
{
  "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

JavaScript
// 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.

JSON
{
  "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

JavaScript
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 slug

4. 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}

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

JavaScript
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}

JavaScript
// 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}

JavaScript
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

JavaScript
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);
});
JSON
{
  "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.

JSON
{
  "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

JavaScript
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:

JSON
{ "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
Last updated: 4/28/2026