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
| Service | URL |
|---|---|
| Content | https://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:
| Parameter | Type | Description |
|---|---|---|
unique_id | string | Template 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:
| Parameter | Type | Description |
|---|---|---|
unique_id | string | Post UUID or slug |
template_unique_id | string | Template 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
| Field | Type | Description |
|---|---|---|
valid | boolean | Whether content passes all required validations |
missing_required | array | Required sections/fields that are missing |
missing_optional | array | Optional sections/fields that are missing |
warnings | array | Non-blocking issues (type mismatches, empty values) |
completion.required | number | Percentage of required fields completed |
completion.total | number | Percentage of all fields completed |
Warning Types
| Type | Description |
|---|---|
type_mismatch | Field value type doesn't match expected type |
empty_value | Field exists but has empty/null value |
nested_field_missing | Required nested field is missing |
invalid_array_item | Array item doesn't match expected structure |
Template Schema Structure
Templates define sections with the following properties:
| Property | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Unique identifier for the section |
label | string | Yes | Human-readable section name |
required | boolean | Yes | Whether section is required for validation |
type | string | Yes | Data type: object, array, string, number, boolean |
fields | array | No | Nested 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
| Type | Description | Options |
|---|---|---|
text | Single-line text | minLength, maxLength, pattern |
textarea | Multi-line text | minLength, maxLength, rows |
richtext | WYSIWYG editor | toolbar, embeds, maxLength |
markdown | Markdown text | preview, toolbar |
slug | URL-friendly text | source, unique |
Number Fields
| Type | Description | Options |
|---|---|---|
number | Integer | min, max, step |
decimal | Float/decimal | min, max, precision |
currency | Money value | currency, min, max |
Date & Time Fields
| Type | Description | Options |
|---|---|---|
date | Date only | minDate, maxDate, format |
time | Time only | minTime, maxTime, format |
datetime | Date and time | minDateTime, maxDateTime |
Selection Fields
| Type | Description | Options |
|---|---|---|
boolean | True/false | default |
select | Single select | options |
multiselect | Multiple select | options, min, max |
radio | Radio buttons | options |
checkbox | Checkboxes | options |
Media Fields
| Type | Description | Options |
|---|---|---|
media | Single file | accept, maxSize |
gallery | Multiple files | accept, maxSize, maxFiles |
image | Image with transforms | accept, transforms |
Relation Fields
| Type | Description | Options |
|---|---|---|
reference | Single reference | ref, display |
references | Multiple references | ref, min, max |
Structured Fields
| Type | Description | Options |
|---|---|---|
json | Raw JSON | schema |
object | Nested object | fields |
array | Array of items | items, min, max |
component | Reusable block | components |
Special Fields
| Type | Description | Options |
|---|---|---|
tags | Tag list | suggestions, max |
color | Color picker | format, presets |
location | GPS coordinates | map |
uid | Auto-generated ID | prefix |
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:
| Parameter | Type | Description |
|---|---|---|
locale | string | Locale code (e.g., en-US, es-ES) |
include | string | Include relations: author,tags |
version | integer | Specific version number |
List Entries
Get paginated entries for a content type.
GET /api/v1/content/:content_type/entries
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
page[number] | integer | Page number (default: 1) |
page[size] | integer | Items per page (default: 25, max: 100) |
filter[status] | string | Filter by status: draft, published, archived |
filter[locale] | string | Filter by locale |
filter[author] | string | Filter by author ID |
filter[tags] | string | Filter by tags (comma-separated) |
filter[created_after] | datetime | Created after date |
filter[created_before] | datetime | Created before date |
sort | string | Sort: created_at, -created_at, published_at, title |
search | string | Full-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:
| Parameter | Type | Description |
|---|---|---|
from | integer | From version number |
to | integer | To 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):
| Field | Type | Description |
|---|---|---|
file | file | The file to upload |
folder | string | Optional folder path |
alt | string | Alt text for images |
title | string | Title/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:
| Parameter | Type | Description |
|---|---|---|
filter[mime_type] | string | Filter by MIME type |
filter[folder] | string | Filter by folder |
sort | string | Sort: 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:
| Parameter | Description |
|---|---|
w | Width in pixels |
h | Height in pixels |
fit | cover, contain, fill, inside, outside |
q | Quality (1-100) |
format | webp, avif, jpg, png |
blur | Blur amount (0-100) |
sharpen | Sharpen amount |
grayscale | Convert to grayscale |
Webhooks
Webhook Events
| Event | Description |
|---|---|
entry.created | New entry created |
entry.updated | Entry updated |
entry.published | Entry published |
entry.unpublished | Entry unpublished |
entry.deleted | Entry deleted |
media.uploaded | Media uploaded |
media.deleted | Media deleted |
content_type.created | Content type created |
content_type.updated | Content 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
| Code | Description |
|---|---|
CONTENT001 | Content type not found |
CONTENT002 | Entry not found |
CONTENT003 | Validation failed |
CONTENT004 | Duplicate slug |
CONTENT005 | Referenced entry not found |
CONTENT006 | Invalid locale |
CONTENT007 | Version not found |
CONTENT008 | Media not found |
CONTENT009 | File too large |
CONTENT010 | Invalid file type |
Rate Limits
| Plan | Requests/minute | Content Types | Entries | Media Storage |
|---|---|---|---|---|
| Free | 60 | 5 | 1,000 | 1 GB |
| Starter | 300 | 20 | 10,000 | 10 GB |
| Pro | 1,000 | 50 | 100,000 | 100 GB |
| Enterprise | Custom | Unlimited | Unlimited | Unlimited |
Related Resources
- Marketing Page - Feature overview and pricing
- Auth Block - User authentication for content access
- Files Block - Media storage and CDN delivery
- Forms Block - Embed forms in content
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