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: Use the same URL for all environments. Your API key determines the environment:
pk_test_*/sk_test_*→ Routes to Stagingpk_live_*/sk_live_*→ Routes to Production
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