Post Types API
The Post Types API manages custom content type definitions (CPT) and pairs with the Blog posts API for typed custom_fields.
This page is written to help developers enable CPT safely and call the right routes quickly.
How to read this page
Each route is also documented as an operation page under Post Types operations:
- what it does
- who can call it
- request shape
- response and error codes
Base path
All CPT definition routes live under /api/v1/post-types.
Related Blog routes live under /api/v1/posts (?type=, body post_type / custom_fields).
Enable first
Installing the Module Hub schema does not turn CPT on.
BLOG_ENABLED=true
POST_TYPES_ENABLED=true
# optional MCU: POST_TYPES_MCU=post-types://enabledIf disabled, every /api/v1/post-types call returns 503 with code: MODULE_DISABLED.
Install schema (after Blog). Prefer dry-run first, then run. Always send Content-Type: application/json and plain ASCII JSON (no rich-text / NBSP spaces from copy-paste).
POST /api/v1/modules/install/post-types/dry-run
Content-Type: application/json
X-Session-ID: <session>{
"target": "all",
"runtime_scope": "flowless-core"
}POST /api/v1/modules/install/post-types
Content-Type: application/json
X-Session-ID: <session>{
"target": "all",
"runtime_scope": "flowless-core"
}target: "all" installs core + ext-seo-per-type + ext-type-restrictions. target: "core" installs only core (ignores components). Empty body {} is valid and defaults to target: "core".
| Error message | Meaning |
|---|---|
Invalid JSON body | Body failed JSON.parse (NBSP/smart quotes, bad Content-Type, truncated JSON) |
Invalid input (+ fields) | JSON parsed, but Zod rejected a field |
Sample type definitions are documented on the Post Types Examples page — Module Hub never auto-installs them.
What the API is for
- Listing and reading type definitions
- Creating / updating / deleting types (admin)
- Fetching public
field_schemafor admin UIs - Stats per type (admin)
- Listing posts of a type via proxy
Route map
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/v1/post-types | Public | List types (?active=false includes inactive) |
POST | /api/v1/post-types | Session + CPT admin | Create a type definition |
GET | /api/v1/post-types/{key} | Public | Get one definition |
PUT | /api/v1/post-types/{key} | Session + CPT admin | Update definition (type_key immutable) |
DELETE | /api/v1/post-types/{key} | Session + CPT admin | Delete (not built-in; not if posts exist) |
GET | /api/v1/post-types/{key}/schema | Public | Schema for editors (field_schema, behaviors, icon…) |
GET | /api/v1/post-types/{key}/stats | Session + CPT admin | Counts by status, lang, author |
GET | /api/v1/post-types/{key}/posts | Optional | Internal proxy → GET /api/v1/posts?type={key} |
Blog routes used with CPT
| Method | Path | CPT fields |
|---|---|---|
GET | /api/v1/posts | Query type (+ lang, filters) |
POST | /api/v1/posts | Body post_type, custom_fields |
PUT | /api/v1/posts/{id} | Body custom_fields (post_type locked) |
See Blog operations.
Example: list types
GET /api/v1/post-types{
"data": [
{
"id": "ptd_1",
"type_key": "case-study",
"type_name": "Case Study",
"field_schema": [],
"behaviors": { "enables_scheduling": true },
"is_active": true,
"is_built_in": false,
"sort_order": 100
}
],
"meta": { "total": 1 }
}Example: create type
POST /api/v1/post-types
X-Session-ID: <session>
Content-Type: application/json{
"type_key": "case-study",
"type_name": "Case Study",
"description": "Customer success / case study",
"slug_prefix": "case-studies/",
"icon": "briefcase",
"color": "#10B981",
"is_active": true,
"sort_order": 10,
"field_schema": [
{
"name": "client_name",
"label": "Client Name",
"type": "text",
"required": true,
"max_length": 120
},
{
"name": "industry",
"label": "Industry",
"type": "select",
"required": false,
"options": ["Tech", "Finance", "Healthcare", "Retail", "Education", "SaaS", "E-commerce"]
},
{
"name": "challenge",
"label": "The Challenge",
"type": "textarea",
"required": false
},
{
"name": "solution",
"label": "Our Solution",
"type": "textarea",
"required": false
},
{
"name": "results",
"label": "Results Achieved",
"type": "textarea",
"required": true
},
{
"name": "duration",
"label": "Project Duration",
"type": "text",
"required": false
},
{
"name": "project_url",
"label": "Live Project URL",
"type": "url",
"required": false
},
{
"name": "technologies",
"label": "Technologies Used",
"type": "text",
"required": false
}
],
"behaviors": {
"public_by_default": true,
"enables_seo": true,
"enables_comments": false,
"enables_reactions": true,
"enables_bookmarks": true,
"enables_views": true,
"enables_scheduling": true,
"requires_approval": true,
"allows_password_visibility": false,
"default_visibility": "public",
"allowed_body_formats": ["markdown", "html", "tiptap_json"]
}
}Full sample catalog (7 types): Post Types Examples.
Example: schema for editors
GET /api/v1/post-types/case-study/schemaReturns field_schema, behaviors, icon, slug_prefix, color for dynamic forms.
field_schema item shape
{
"name": "client_name",
"label": "Client Name",
"type": "text",
"required": true,
"max_length": 120,
"placeholder": "ACME",
"options": ["Tech", "Finance"]
}Types: text | textarea | number | url | date | select | multi-select | boolean
Behaviors (common)
| Key | Effect |
|---|---|
requires_approval | Non-admins cannot publish |
allows_password_visibility | Allow visibility=password |
default_visibility | Used when visibility omitted on create |
enables_scheduling | Allow scheduled_at / schedule |
allowed_body_formats | Restrict body_format |
max_per_author | Cap posts of this type per author |
Error codes
| Code | When |
|---|---|
MODULE_DISABLED | CPT env/MCU off (503) |
USER_TYPES_DISABLED | POST_TYPES_ALLOW_USER_TYPES=false |
POST_TYPE_LOCKED | Changing post_type after create |
APPROVAL_REQUIRED | Non-admin publish when approval required |
MAX_PER_AUTHOR | Author over type quota |
CUSTOM_FIELDS_TOO_LARGE / validation errors | Bad or oversized custom_fields |
Env reference
| Var | Default | Notes |
|---|---|---|
POST_TYPES_ENABLED | false | Master switch |
POST_TYPES_MCU | `` | post-types://enabled@?... |
POST_TYPES_REQUIRE_APPROVAL | false | Global approval |
POST_TYPES_DEFAULT_TYPE | post | Default post_type |
POST_TYPES_ALLOW_USER_TYPES | true | Allow create via API |
POST_TYPES_CUSTOM_FIELDS_MAX_KB | 10 | Cap |
POST_TYPES_FIELD_SCHEMA_MAX_KB | 64 | Cap |
POST_TYPES_ADMIN_ROLES | (blog admin) | Type CRUD + publish under approval |
POST_TYPES_CACHE_TTL | 86400 | Def cache TTL |
POST_TYPES_FRESH_CACHE_MODE | true | Bypass Redis when true |
Operation pages
- List Post Types
- Create Post Type
- Get Post Type
- Update Post Type
- Delete Post Type
- Get Post Type Schema
- Get Post Type Stats
- List Posts by Type