Skip to main content

Content Block

Content Block

Headless CMS API with custom content types, versioning, and localization

The Content Block provides a complete headless CMS solution. Define unlimited custom content types, manage content with a rich editor, track versions, support multiple languages, and deliver content to any frontend via API.

Key Features​

  • Custom Content Types - Define any content structure with flexible fields
  • Rich Text Editor - Full WYSIWYG with embeds, tables, and custom components
  • Post Templates & Validation - Define structured content schemas with real-time validation and completion tracking
  • Media Management - Upload, organize, and transform images and files
  • Content Versioning - Full version history with diff, restore, and audit
  • Publishing Workflows - Draft, review, schedule, and publish with approvals
  • Localization - Multi-language content with translation management
  • API-first Delivery - RESTful API for any frontend
  • Real-time Webhooks - Instant notifications for content changes

API Endpoint​

ServiceURL
Contenthttps://content.api.us.23blocks.com

Environment Routing: One URL, one environment. All shared-platform API keys are live keys:

  • pk_live_* / sk_live_* → your production tenant. Need a separate staging environment? It is available as a paid add-on on deployments in your own cloud account — see 23blocks in a Box.

Quick Start​

Installation​

npm install @23blocks/sdk

Basic Usage​

import { create23BlocksClient } from '@23blocks/sdk';

const client = create23BlocksClient({
urls: { content: 'https://content.api.us.23blocks.com' },
apiKey: 'your-api-key',
});

// Define a content type
const blogType = await client.content.createType({
name: 'Blog Post',
slug: 'blog-posts',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'content', type: 'richtext' },
{ name: 'author', type: 'reference', ref: 'authors' }
]
});

// Create content entry
const post = await client.content.create('blog-posts', {
title: 'Getting Started with Headless CMS',
content: '<p>Learn how to build content-driven applications...</p>',
author: 'author_abc123'
});

// Fetch published content
const posts = await client.content.list('blog-posts', {
status: 'published',
limit: 10
});

Authentication​

Required Headers​

All API requests require authentication:

Authorization: Bearer [JWT_TOKEN]
X-API-Key: [COMPANY_API_KEY]
Content-Type: application/json

API Reference​


Content Types​

Create Content Type​

Define a new content type with field definitions.

POST /api/v1/content-types

Request Body:

{
"data": {
"type": "content-types",
"attributes": {
"name": "Blog Post",
"slug": "blog-posts",
"description": "Blog articles and news",
"fields": [
{
"name": "title",
"type": "text",
"label": "Title",
"required": true,
"localized": true,
"validation": {
"minLength": 5,
"maxLength": 200
}
},
{
"name": "slug",
"type": "slug",
"label": "URL Slug",
"required": true,
"unique": true,
"source": "title"
},
{
"name": "content",
"type": "richtext",
"label": "Content",
"localized": true
},
{
"name": "featuredImage",
"type": "media",
"label": "Featured Image",
"accept": ["image/*"]
},
{
"name": "author",
"type": "reference",
"label": "Author",
"ref": "authors",
"required": true
},
{
"name": "tags",
"type": "tags",
"label": "Tags"
},
{
"name": "publishedAt",
"type": "datetime",
"label": "Publish Date"
}
],
"settings": {
"preview_url": "https://mysite.com/preview/{{slug}}",
"title_field": "title",
"enable_versioning": true
}
}
}
}

Response:

{
"data": {
"id": "ct_xyz789",
"type": "content-types",
"attributes": {
"name": "Blog Post",
"slug": "blog-posts",
"description": "Blog articles and news",
"fields": [...],
"settings": {...},
"entry_count": 0,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}
}

List Content Types​

GET /api/v1/content-types

Get Content Type​

GET /api/v1/content-types/:slug

Update Content Type​

PATCH /api/v1/content-types/:slug

Delete Content Type​

DELETE /api/v1/content-types/:slug

Post Templates​

Post Templates define structured content schemas that enable form-based content creation with real-time validation. Use templates to enforce content structure, track completion progress, and ensure quality before publishing.

List Post Templates​

Get all available post templates.

GET /api/v1/post_templates

Response:

{
"data": [
{
"id": "pt_abc123",
"type": "post_templates",
"attributes": {
"unique_id": "pt_abc123",
"slug": "blog-article",
"name": "Blog Article",
"description": "Standard blog post with SEO fields",
"schema": {...},
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}
}
]
}

Get Post Template​

Retrieve a template by UUID or slug.

GET /api/v1/post_templates/:unique_id

Parameters:

ParameterTypeDescription
unique_idstringTemplate UUID or slug

Response:

{
"data": {
"id": "pt_abc123",
"type": "post_templates",
"attributes": {
"unique_id": "pt_abc123",
"slug": "blog-article",
"name": "Blog Article",
"description": "Standard blog post with SEO fields",
"schema": {
"sections": [
{
"key": "metadata",
"label": "Metadata",
"required": true,
"type": "object",
"fields": [
{
"key": "title",
"label": "Title",
"type": "string",
"required": true
},
{
"key": "description",
"label": "Meta Description",
"type": "string",
"required": true
}
]
},
{
"key": "content",
"label": "Main Content",
"required": true,
"type": "object",
"fields": [
{
"key": "body",
"label": "Article Body",
"type": "string",
"required": true
},
{
"key": "summary",
"label": "Summary",
"type": "string",
"required": false
}
]
},
{
"key": "tags",
"label": "Tags",
"required": false,
"type": "array"
}
]
},
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
}
}
}

Create Post Template​

Create a new post template with a content schema.

POST /api/v1/post_templates

Request Body:

{
"data": {
"type": "post_templates",
"attributes": {
"name": "Product Review",
"slug": "product-review",
"description": "Template for product review articles",
"schema": {
"sections": [
{
"key": "product_info",
"label": "Product Information",
"required": true,
"type": "object",
"fields": [
{
"key": "name",
"label": "Product Name",
"type": "string",
"required": true
},
{
"key": "rating",
"label": "Rating",
"type": "number",
"required": true
},
{
"key": "price",
"label": "Price",
"type": "number",
"required": false
}
]
},
{
"key": "review_content",
"label": "Review Content",
"required": true,
"type": "object",
"fields": [
{
"key": "pros",
"label": "Pros",
"type": "array",
"required": true
},
{
"key": "cons",
"label": "Cons",
"type": "array",
"required": true
},
{
"key": "verdict",
"label": "Verdict",
"type": "string",
"required": true
}
]
},
{
"key": "media",
"label": "Media",
"required": false,
"type": "object",
"fields": [
{
"key": "images",
"label": "Product Images",
"type": "array",
"required": false
}
]
}
]
}
}
}
}

Update Post Template​

Update an existing template.

PUT /api/v1/post_templates/:unique_id

Delete Post Template​

Remove a template.

DELETE /api/v1/post_templates/:unique_id

Content Validation​

Validate post content against a template schema. Use this to provide real-time validation feedback and track content completion.

Validate Post Content​

Validate a post's content against a template schema.

PUT /api/v1/posts/:unique_id/validate?template_unique_id=:template_id

Parameters:

ParameterTypeDescription
unique_idstringPost UUID or slug
template_unique_idstringTemplate UUID or slug to validate against

Response:

{
"data": {
"valid": false,
"missing_required": [
{
"key": "metadata.description",
"label": "Meta Description",
"section": "metadata"
}
],
"missing_optional": [
{
"key": "tags",
"label": "Tags",
"section": "tags"
},
{
"key": "content.summary",
"label": "Summary",
"section": "content"
}
],
"warnings": [
{
"key": "metadata.title",
"message": "Value is empty",
"type": "empty_value"
},
{
"key": "content.body",
"message": "Expected string but got number",
"type": "type_mismatch"
}
],
"completion": {
"required": 75,
"total": 60
}
}
}

Validation Response Fields​

FieldTypeDescription
validbooleanWhether content passes all required validations
missing_requiredarrayRequired sections/fields that are missing
missing_optionalarrayOptional sections/fields that are missing
warningsarrayNon-blocking issues (type mismatches, empty values)
completion.requirednumberPercentage of required fields completed
completion.totalnumberPercentage of all fields completed

Warning Types​

TypeDescription
type_mismatchField value type doesn't match expected type
empty_valueField exists but has empty/null value
nested_field_missingRequired nested field is missing
invalid_array_itemArray item doesn't match expected structure

Template Schema Structure​

Templates define sections with the following properties:

PropertyTypeRequiredDescription
keystringYesUnique identifier for the section
labelstringYesHuman-readable section name
requiredbooleanYesWhether section is required for validation
typestringYesData type: object, array, string, number, boolean
fieldsarrayNoNested field definitions (for object type)

SDK Example​

import { create23BlocksClient } from '@23blocks/sdk';

const client = create23BlocksClient({
urls: { content: 'https://content.api.us.23blocks.com' },
apiKey: process.env.BLOCKS_API_KEY!,
});

// Create a post template
const template = await client.content.createTemplate({
name: 'Blog Article',
slug: 'blog-article',
schema: {
sections: [
{
key: 'metadata',
label: 'Metadata',
required: true,
type: 'object',
fields: [
{ key: 'title', label: 'Title', type: 'string', required: true },
{ key: 'description', label: 'Description', type: 'string', required: true }
]
},
{
key: 'content',
label: 'Content',
required: true,
type: 'object',
fields: [
{ key: 'body', label: 'Body', type: 'string', required: true }
]
}
]
}
});

// Validate post content against template
const validation = await client.content.validatePost(postId, {
templateId: template.id
});

if (!validation.valid) {
console.log('Missing required fields:', validation.missing_required);
console.log('Warnings:', validation.warnings);
console.log('Completion:', validation.completion.required + '%');
}

Field Types​

Content Block supports 50+ field types:

Text Fields​

TypeDescriptionOptions
textSingle-line textminLength, maxLength, pattern
textareaMulti-line textminLength, maxLength, rows
richtextWYSIWYG editortoolbar, embeds, maxLength
markdownMarkdown textpreview, toolbar
slugURL-friendly textsource, unique

Number Fields​

TypeDescriptionOptions
numberIntegermin, max, step
decimalFloat/decimalmin, max, precision
currencyMoney valuecurrency, min, max

Date & Time Fields​

TypeDescriptionOptions
dateDate onlyminDate, maxDate, format
timeTime onlyminTime, maxTime, format
datetimeDate and timeminDateTime, maxDateTime

Selection Fields​

TypeDescriptionOptions
booleanTrue/falsedefault
selectSingle selectoptions
multiselectMultiple selectoptions, min, max
radioRadio buttonsoptions
checkboxCheckboxesoptions

Media Fields​

TypeDescriptionOptions
mediaSingle fileaccept, maxSize
galleryMultiple filesaccept, maxSize, maxFiles
imageImage with transformsaccept, transforms

Relation Fields​

TypeDescriptionOptions
referenceSingle referenceref, display
referencesMultiple referencesref, min, max

Structured Fields​

TypeDescriptionOptions
jsonRaw JSONschema
objectNested objectfields
arrayArray of itemsitems, min, max
componentReusable blockcomponents

Special Fields​

TypeDescriptionOptions
tagsTag listsuggestions, max
colorColor pickerformat, presets
locationGPS coordinatesmap
uidAuto-generated IDprefix

Content Entries​

Create Entry​

Create a new content entry.

POST /api/v1/content/:content_type/entries

Request Body:

{
"data": {
"type": "entries",
"attributes": {
"fields": {
"title": "Getting Started with Headless CMS",
"slug": "getting-started-headless-cms",
"content": "<p>Learn how to build content-driven applications with our headless CMS API...</p>",
"featuredImage": "file_abc123",
"author": "author_xyz789",
"tags": ["tutorial", "cms", "api"],
"publishedAt": "2025-01-15T10:00:00Z"
},
"status": "draft",
"locale": "en-US"
}
}
}

Response:

{
"data": {
"id": "entry_def456",
"type": "entries",
"attributes": {
"content_type": "blog-posts",
"fields": {
"title": "Getting Started with Headless CMS",
"slug": "getting-started-headless-cms",
"content": "<p>Learn how to build content-driven applications...</p>",
"featuredImage": {
"id": "file_abc123",
"url": "https://cdn.23blocks.com/...",
"width": 1200,
"height": 630
},
"author": {
"id": "author_xyz789",
"name": "Jane Smith"
},
"tags": ["tutorial", "cms", "api"],
"publishedAt": "2025-01-15T10:00:00Z"
},
"status": "draft",
"locale": "en-US",
"version": 1,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"created_by": "user_abc123"
}
}
}

Get Entry​

Retrieve an entry by ID or slug.

GET /api/v1/content/:content_type/entries/:id

Query Parameters:

ParameterTypeDescription
localestringLocale code (e.g., en-US, es-ES)
includestringInclude relations: author,tags
versionintegerSpecific version number

List Entries​

Get paginated entries for a content type.

GET /api/v1/content/:content_type/entries

Query Parameters:

ParameterTypeDescription
page[number]integerPage number (default: 1)
page[size]integerItems per page (default: 25, max: 100)
filter[status]stringFilter by status: draft, published, archived
filter[locale]stringFilter by locale
filter[author]stringFilter by author ID
filter[tags]stringFilter by tags (comma-separated)
filter[created_after]datetimeCreated after date
filter[created_before]datetimeCreated before date
sortstringSort: created_at, -created_at, published_at, title
searchstringFull-text search query

Update Entry​

Update an existing entry.

PATCH /api/v1/content/:content_type/entries/:id

Delete Entry​

Delete an entry (soft delete by default).

DELETE /api/v1/content/:content_type/entries/:id

Duplicate Entry​

Create a copy of an entry.

POST /api/v1/content/:content_type/entries/:id/duplicate

Publishing​

Publish Entry​

Publish an entry.

POST /api/v1/content/:content_type/entries/:id/publish

Request Body:

{
"publish_at": null,
"locales": ["en-US", "es-ES"]
}

Unpublish Entry​

Unpublish an entry.

POST /api/v1/content/:content_type/entries/:id/unpublish

Schedule Publication​

Schedule an entry to publish later.

POST /api/v1/content/:content_type/entries/:id/schedule

Request Body:

{
"publish_at": "2025-02-01T09:00:00Z",
"unpublish_at": "2025-02-28T23:59:59Z"
}

Get Publishing Status​

GET /api/v1/content/:content_type/entries/:id/status

Versioning​

Get Version History​

GET /api/v1/content/:content_type/entries/:id/versions

Response:

{
"data": [
{
"version": 5,
"status": "published",
"created_at": "2025-01-15T14:00:00Z",
"created_by": {
"id": "user_abc123",
"name": "Jane Smith"
},
"changes": ["Updated content", "Changed featured image"]
},
{
"version": 4,
"status": "draft",
"created_at": "2025-01-15T12:00:00Z",
"created_by": {
"id": "user_abc123",
"name": "Jane Smith"
},
"changes": ["Added tags"]
}
]
}

Get Specific Version​

GET /api/v1/content/:content_type/entries/:id/versions/:version

Compare Versions​

GET /api/v1/content/:content_type/entries/:id/versions/compare

Query Parameters:

ParameterTypeDescription
fromintegerFrom version number
tointegerTo version number

Restore Version​

POST /api/v1/content/:content_type/entries/:id/versions/:version/restore

Localization​

Get Available Locales​

GET /api/v1/locales

Create Localized Entry​

POST /api/v1/content/:content_type/entries/:id/locales/:locale

Request Body:

{
"data": {
"fields": {
"title": "Primeros pasos con CMS sin cabeza",
"content": "<p>Aprende a crear aplicaciones...</p>"
}
}
}

Get Entry in Locale​

GET /api/v1/content/:content_type/entries/:id?locale=es-ES

List Entry Locales​

GET /api/v1/content/:content_type/entries/:id/locales

Delete Locale​

DELETE /api/v1/content/:content_type/entries/:id/locales/:locale

Media​

Upload Media​

POST /api/v1/media

Request Body (multipart/form-data):

FieldTypeDescription
filefileThe file to upload
folderstringOptional folder path
altstringAlt text for images
titlestringTitle/caption

Response:

{
"data": {
"id": "file_abc123",
"type": "media",
"attributes": {
"filename": "hero-image.jpg",
"mime_type": "image/jpeg",
"size": 245678,
"url": "https://cdn.23blocks.com/uploads/hero-image.jpg",
"width": 1920,
"height": 1080,
"alt": "Hero image",
"folder": "blog/images",
"created_at": "2025-01-15T10:30:00Z"
}
}
}

Get Media​

GET /api/v1/media/:id

List Media​

GET /api/v1/media

Query Parameters:

ParameterTypeDescription
filter[mime_type]stringFilter by MIME type
filter[folder]stringFilter by folder
sortstringSort: created_at, size, filename

Delete Media​

DELETE /api/v1/media/:id

Image Transformations​

Append transforms to image URLs:

https://cdn.23blocks.com/uploads/hero-image.jpg?w=800&h=600&fit=cover&q=80

Transform Parameters:

ParameterDescription
wWidth in pixels
hHeight in pixels
fitcover, contain, fill, inside, outside
qQuality (1-100)
formatwebp, avif, jpg, png
blurBlur amount (0-100)
sharpenSharpen amount
grayscaleConvert to grayscale

Webhooks​

Webhook Events​

EventDescription
entry.createdNew entry created
entry.updatedEntry updated
entry.publishedEntry published
entry.unpublishedEntry unpublished
entry.deletedEntry deleted
media.uploadedMedia uploaded
media.deletedMedia deleted
content_type.createdContent type created
content_type.updatedContent type updated

Create Webhook​

POST /api/v1/webhooks

Request Body:

{
"data": {
"type": "webhooks",
"attributes": {
"url": "https://api.example.com/content-webhook",
"events": ["entry.published", "entry.updated"],
"content_types": ["blog-posts"],
"secret": "webhook-secret-key"
}
}
}

Webhook Payload​

{
"event": "entry.published",
"timestamp": "2025-01-15T14:30:00Z",
"data": {
"entry_id": "entry_def456",
"content_type": "blog-posts",
"locale": "en-US",
"version": 5,
"fields": {
"title": "Getting Started with Headless CMS",
"slug": "getting-started-headless-cms"
}
}
}

SDK Examples​

TypeScript​

import { create23BlocksClient } from '@23blocks/sdk';

const client = create23BlocksClient({
urls: { content: 'https://content.api.us.23blocks.com' },
apiKey: process.env.BLOCKS_API_KEY!,
});

// Create a blog post
async function createBlogPost(data: BlogPostData) {
const entry = await client.content.create('blog-posts', {
fields: {
title: data.title,
slug: data.slug,
content: data.content,
author: data.authorId,
tags: data.tags
},
status: 'draft'
});

return entry;
}

// Publish with localization
async function publishPost(entryId: string) {
// Publish English version
await client.content.publish('blog-posts', entryId, {
locales: ['en-US']
});

// Create Spanish translation
await client.content.createLocale('blog-posts', entryId, 'es-ES', {
fields: {
title: 'Spanish Title',
content: '<p>Spanish content...</p>'
}
});

// Publish Spanish version
await client.content.publish('blog-posts', entryId, {
locales: ['es-ES']
});
}

// Fetch content with includes
async function getPostWithAuthor(slug: string, locale: string = 'en-US') {
const posts = await client.content.list('blog-posts', {
filter: { slug, status: 'published' },
locale,
include: ['author', 'featuredImage']
});

return posts.data[0] || null;
}

// Full-text search
async function searchPosts(query: string) {
const results = await client.content.list('blog-posts', {
search: query,
filter: { status: 'published' },
sort: '-published_at',
limit: 20
});

return results;
}

Error Codes​

CodeDescription
CONTENT001Content type not found
CONTENT002Entry not found
CONTENT003Validation failed
CONTENT004Duplicate slug
CONTENT005Referenced entry not found
CONTENT006Invalid locale
CONTENT007Version not found
CONTENT008Media not found
CONTENT009File too large
CONTENT010Invalid file type

Rate Limits​

PlanRequests/minuteContent TypesEntriesMedia Storage
Free6051,0001 GB
Starter3002010,00010 GB
Pro1,00050100,000100 GB
EnterpriseCustomUnlimitedUnlimitedUnlimited

Use This Block with AI Agents​

This block ships an official Claude Code plugin from the public 23blocks marketplace (19 plugins, 205 skills). Your agent gets full working knowledge of this API — endpoints, identity registration, and conventions.

# one-time: add the 23blocks marketplace
claude plugin marketplace add https://github.com/23blocks-OS/ai-agents

# install this block's plugin
claude plugin install content-block

Configure BLOCKS_API_URL, BLOCKS_API_KEY, and BLOCKS_AUTH_TOKEN. Register identity first: each block is autonomous — register the user with this block's identity endpoint before private calls (only user_unique_id is required; email/phone are optional notification-routing fields).

Full reference: AI Agents & Claude Code Plugins · github.com/23blocks-OS/ai-agents