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: Use the same URL for all environments. Your API key determines the environment:

  • pk_test_* / sk_test_* → Routes to Staging
  • pk_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:

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