HOME/DOCS/CONTENT API

Content API

Full CRUD for content entries. All endpoints are scoped to an app. Endpoints differ slightly for collection vs single schemas.

Collection endpoints

List entries

GET /api/v1/apps/:app_slug/content/:schema_slug

By default, only published entries are returned. Use ?status=all to include drafts, or ?status=draft for drafts only. Use ?q=search to filter entries by text.

curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/api/v1/apps/my-app/content/blog-posts

Response:

[
  {
    "id": "my-first-post",
    "data": {
      "title": "My First Post",
      "body": "Hello world",
      "published": true
    }
  }
]

Get an entry

GET /api/v1/apps/:app_slug/content/:schema_slug/:entry_id
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/api/v1/apps/my-app/content/blog-posts/my-first-post

Response -- the entry data directly (no wrapper):

{
  "title": "My First Post",
  "body": "Hello world",
  "published": true
}

Returns 404 if the entry does not exist.

Create an entry

POST /api/v1/apps/:app_slug/content/:schema_slug
Content-Type: application/json

Requires editor role or above.

curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "New Post", "body": "Content here"}' \
  http://localhost:3000/api/v1/apps/my-app/content/blog-posts

Response (201 Created):

{
  "id": "new-post"
}

The entry ID is generated from the content (see entry ID generation).

Validation errors return 400:

{
  "errors": ["title: \"title\" is a required property"]
}

Update an entry

PUT /api/v1/apps/:app_slug/content/:schema_slug/:entry_id
Content-Type: application/json

Requires editor role or above. A version history snapshot is saved before updating.

curl -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated Post", "body": "New content", "published": true}' \
  http://localhost:3000/api/v1/apps/my-app/content/blog-posts/new-post

Returns 200 OK on success.

Delete an entry

DELETE /api/v1/apps/:app_slug/content/:schema_slug/:entry_id

Requires editor role or above.

curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/api/v1/apps/my-app/content/blog-posts/new-post

Returns 204 No Content on success.

Publish / Unpublish

POST /api/v1/apps/:app_slug/content/:schema_slug/:entry_id/publish
POST /api/v1/apps/:app_slug/content/:schema_slug/:entry_id/unpublish

See Publish API for details.

Version history

List saved versions of an entry:

GET /api/v1/apps/:app_slug/content/:schema_slug/:entry_id/versions

Get a specific version by timestamp:

GET /api/v1/apps/:app_slug/content/:schema_slug/:entry_id/versions/:timestamp

Revert an entry to a previous version:

POST /api/v1/apps/:app_slug/content/:schema_slug/:entry_id/versions/:timestamp/revert

Requires editor role or above.

Bulk operations

Create, update, or delete multiple entries in a single request. All bulk endpoints require editor role or above.

POST /api/v1/apps/:app_slug/content/:schema_slug/_bulk/create
POST /api/v1/apps/:app_slug/content/:schema_slug/_bulk/update
POST /api/v1/apps/:app_slug/content/:schema_slug/_bulk/delete
POST /api/v1/apps/:app_slug/content/:schema_slug/_bulk/publish
POST /api/v1/apps/:app_slug/content/:schema_slug/_bulk/unpublish

Single endpoints

For schemas with kind: "single", use the /single endpoints instead:

Get

GET /api/v1/apps/:app_slug/content/:schema_slug/single
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/api/v1/apps/my-app/content/site-settings/single

Create or update

PUT /api/v1/apps/:app_slug/content/:schema_slug/single
Content-Type: application/json
curl -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"site_name": "My Site", "tagline": "A great site"}' \
  http://localhost:3000/api/v1/apps/my-app/content/site-settings/single

Delete

DELETE /api/v1/apps/:app_slug/content/:schema_slug/single

Richtext fields

Schema fields with format: "markdown-richtext" are stored as {"markdown": "...", "html": "..."} objects. The API automatically projects these to a plain string in responses:

  • Default: the html value is returned
  • ?render=raw: the markdown value is returned

For example, a stored entry:

{
  "title": "My Post",
  "body": {
    "markdown": "# Hello\n\n![photo](upload:abc123/photo.jpg)",
    "html": "<h1>Hello</h1>\n<img src=\"upload:abc123/photo.jpg\">"
  }
}

Is returned by the API as:

{
  "title": "My Post",
  "body": "<h1>Hello</h1>\n<img src=\"/api/v1/apps/my-app/uploads/abc123/photo.jpg\">"
}

Upload URI resolution

Images and links in richtext HTML use the upload: URI scheme internally (e.g. upload:hash/filename). When returned via the API, these are resolved to the API upload path:

upload:abc123/photo.jpg  →  /api/v1/apps/:app_slug/uploads/abc123/photo.jpg

These paths require bearer token authentication, using the same token as the content request. The resolved paths are root-relative -- prepend your Substrukt instance URL to form the full download URL.

In raw mode (?render=raw), the markdown is returned as-is with upload: URIs unresolved.

Working with uploads in content

When creating or updating content that includes upload fields, use the upload hash reference format:

{
  "title": "Post with Image",
  "cover": {
    "hash": "a1b2c3d4e5f6...",
    "filename": "photo.jpg",
    "mime": "image/jpeg"
  }
}

Upload the file first via the Uploads API, then use the returned hash in your content.