Appearance
Custom Types
Custom Types let you define your own content types beyond the built-in types (page, article, recipe, homepage, landing, and more) provided by EngageHQ.
Overview
Every content item in EngageHQ has a content_type that determines its available fields, allowed sections, and editor layout. The built-in types cover common use cases, but organizations often need specialized content structures — event pages, product showcases, location guides, etc.
Custom Types allow you to:
- Define custom fields — Add structured data fields (text, number, boolean, select, etc.) specific to the content type
- Control section availability — Restrict which built-in content sections and platform elements are available in the editor
- Configure editor layout — Choose which sidebar cards appear (SEO, tags, publishing, etc.)
Admin UI
Custom types can also be created and managed through the Retail Success Platform admin dashboard under Content Studio > Custom Types, without using the API directly.
Required Permissions
All Custom Type management endpoints require a valid JWT with the appropriate engagehq:custom_types.* scope:
| Endpoint | Required Scope |
|---|---|
GET list / active / show | engagehq:custom_types.view |
POST create | engagehq:custom_types.create |
PUT update | engagehq:custom_types.update |
DELETE delete | engagehq:custom_types.delete |
Requests missing the required scope will receive a 403 Forbidden response.
Create Custom Type
Required scope: engagehq:custom_types.create
http
POST /api/v1/content/custom-typesRequest Body
json
{
"type_key": "event_page",
"display_name": "Event Page",
"description": "Pages for promotional events and in-store activities",
"schema": {
"fields": [
{
"key": "event_date",
"label": "Event Date",
"type": "datetime",
"required": true
},
{
"key": "event_location",
"label": "Location",
"type": "text",
"required": false
},
{
"key": "registration_url",
"label": "Registration URL",
"type": "url",
"required": false
}
],
"allowed_slice_types": [
"hero", "rich_text", "image", "gallery", "cta", "form"
],
"sidebar_cards": [
"publishing", "seo", "tags", "featured_image"
]
}
}Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
type_key | string | Yes | Unique identifier for the type. Lowercase letters, numbers, and underscores only (snake_case) — no hyphens. Cannot be changed after creation. |
display_name | string | Yes | Human-readable name shown in the admin UI |
description | string | No | Description of what this content type is for |
schema | object | Yes | Type definition containing fields, allowed sections, and layout config |
schema.fields | array | Yes | Array of custom field definitions (see Field Types) |
schema.allowed_slice_types | array | Yes | Which built-in section types are available in the editor |
schema.sidebar_cards | array | Yes | Which sidebar cards appear in the editor |
ui_schema | object | No | Additional UI configuration hints |
validation_rules | object | No | Custom validation rules for the type's fields |
default_values | object | No | Default values for custom fields when creating new content |
Response
json
{
"success": true,
"data": {
"id": "cty_00000k1L2m3N4o5",
"type_key": "event_page",
"display_name": "Event Page",
"description": "Pages for promotional events and in-store activities",
"schema": {
"fields": [
{
"key": "event_date",
"label": "Event Date",
"type": "datetime",
"required": true
},
{
"key": "event_location",
"label": "Location",
"type": "text",
"required": false
},
{
"key": "registration_url",
"label": "Registration URL",
"type": "url",
"required": false
}
],
"allowed_slice_types": [
"hero", "rich_text", "image", "gallery", "cta", "form"
],
"sidebar_cards": [
"publishing", "seo", "tags", "featured_image"
]
},
"is_active": true,
"created_at": "2026-03-01T12:00:00+00:00",
"updated_at": "2026-03-01T12:00:00+00:00"
}
}Unique Constraint
The type_key must be unique within your organization. Attempting to create a duplicate returns a 422 validation error.
Management API
Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/content/custom-types | List all custom types |
GET | /api/v1/content/custom-types/active | List active types (lightweight) |
POST | /api/v1/content/custom-types | Create a new custom type |
GET | /api/v1/content/custom-types/{id} | Get a single custom type |
PUT | /api/v1/content/custom-types/{id} | Update a custom type |
DELETE | /api/v1/content/custom-types/{id} | Delete a custom type |
List Custom Types
Required scope: engagehq:custom_types.view
http
GET /api/v1/content/custom-typesQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
search | string | - | Search by display name, type key, or description |
is_active | boolean | - | Filter by active status |
per_page | integer | 20 | Items per page (1-100) |
page | integer | 1 | Page number |
Example Request
javascript
const response = await fetch(
'https://cdn.engagehq.retailsuccessplatform.com/api/v1/content/custom-types',
{
headers: {
'Authorization': `Bearer ${token}`,
'X-Organization-Context': organizationId,
},
}
);List Active Types (Lightweight)
Required scope: engagehq:custom_types.view
Returns only active custom types with minimal fields (type_key and display_name). Useful for populating navigation or content type selectors.
http
GET /api/v1/content/custom-types/activeShow Custom Type
Required scope: engagehq:custom_types.view
http
GET /api/v1/content/custom-types/{id}Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Custom type hashkey (cty_xxxxx) |
Update Custom Type
Required scope: engagehq:custom_types.update
http
PUT /api/v1/content/custom-types/{id}Accepts the same fields as the create endpoint except type_key, which is immutable. Only included fields are updated.
Example Request
javascript
const response = await fetch(
`https://cdn.engagehq.retailsuccessplatform.com/api/v1/content/custom-types/${typeId}`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'X-Organization-Context': organizationId,
'Content-Type': 'application/json',
},
body: JSON.stringify({
display_name: 'Community Event',
schema: {
fields: [
{ key: 'event_date', label: 'Event Date', type: 'date', required: true },
{ key: 'event_location', label: 'Location', type: 'text', required: false },
{ key: 'rsvp_limit', label: 'RSVP Limit', type: 'number', required: false },
],
allowed_slice_types: ['hero', 'rich_text', 'image', 'gallery', 'cta', 'form'],
sidebar_cards: ['publishing', 'seo', 'tags', 'featured_image'],
},
}),
}
);Delete Custom Type
Required scope: engagehq:custom_types.delete
http
DELETE /api/v1/content/custom-types/{id}Deletion Protection
A custom type cannot be deleted if any content items use it. The API returns a 422 error indicating how many content items reference the type. You must delete or reassign all content items before deleting the type.
Schema Definition
The schema object defines the structure of a custom type.
Fields
Each field in schema.fields defines a custom data field that content editors fill in when creating content of this type.
| Property | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Unique field identifier (used in content JSON) |
label | string | Yes | Display label in the editor |
type | string | Yes | Field type (see below) |
required | boolean | No | Whether the field is required (default: false) |
placeholder | string | No | Placeholder text for the input |
help_text | string | No | Help text shown below the input |
default | mixed | No | Default value |
options | array | No | Options for select fields — each entry is a string or a { "value", "label" } object (see Select Options) |
fields | array | No | Sub-field definitions for repeater fields (scalar types only) |
Supported Field Types
| Type | Description | Renders As |
|---|---|---|
text | Single-line text | Text input |
rich_text | Multi-line text | Textarea |
number | Numeric value | Number input |
datetime | Date and time | Datetime picker |
image | Image asset | Media Library picker |
select | Single selection from options | Dropdown |
relation | Reference to another content item | Content picker |
boolean | True/false toggle | Checkbox |
url | URL string | URL input |
email | Email address | Email input |
color | Hex color value | Color picker |
json | Arbitrary JSON value | JSON textarea |
repeater | Repeatable group of sub-fields | Add/remove/reorder item list |
Select Options
A select field's options array accepts either form, and you may mix both in one list:
Bare string — used as both the stored value and the display label:
json"options": ["small", "medium", "large"]{ value, label }object — when the stored value differs from the label authors see:json"options": [ { "value": "mobile", "label": "Mobile menu (drawer)" }, { "value": "desktop_primary_bar", "label": "Main navigation bar" } ]
value is required (non-empty, ≤255 chars) and is stored as the field's value; label is optional (a blank label, or one equal to the value, is stored as a bare string). Repeater sub-fields of type select use the same option shapes.
Repeater Fields
A repeater field defines a fields array of scalar sub-field definitions (same shape as top-level fields, but type must be a scalar type — repeaters cannot be nested). Authors add, remove, and reorder rows in the editor; each row is an object keyed by the sub-field key.
json
{
"key": "ingredients",
"label": "Ingredients",
"type": "repeater",
"fields": [
{ "key": "name", "label": "Name", "type": "text", "required": true },
{ "key": "quantity", "label": "Quantity", "type": "number" }
]
}The stored value for a repeater field is an array of objects, each keyed by its sub-field key:
json
"custom_fields": {
"ingredients": [
{ "name": "Flour", "quantity": 2 },
{ "name": "Sugar", "quantity": 1 }
]
}Allowed Slice Types
The schema.allowed_slice_types array controls which built-in content sections are available in the editor for this type. Valid values:
hero, rich_text, image, video, gallery, features, testimonials, stats, cta, form, team, content_block, html
Sidebar Cards
The schema.sidebar_cards array controls which sidebar panels appear in the content editor. Valid values:
publishing, seo, tags, distribution, notes, featured_image, cache_freshness, related_content
Content Delivery
When content created with a custom type is fetched via the Content API, the item's type field is set to the custom type's type_key. Custom field values are returned under metadata.custom_fields, keyed by the field keys you defined.
Filter the list endpoint by passing the type_key to the type parameter. Because custom field values live in metadata, request them with include=metadata on the list endpoint (the single-item /{slug} endpoint always includes metadata):
http
GET /api/v1/public/content?type=event_page&include=metadatajson
{
"id": "con_00000a1B2c3D4e5",
"type": "event_page",
"slug": "summer-bbq-bash",
"title": "Summer BBQ Bash",
"metadata": {
"custom_fields": {
"event_date": "2026-07-04",
"event_location": "Store #42 Parking Lot",
"registration_url": "https://example.com/rsvp"
}
}
}Response field names
The identifier is returned as id (not content_id) and the type as type (not content_type). Custom field values are nested under metadata.custom_fields, not at the top level, and the list endpoint only returns metadata when you pass include=metadata.
Consuming platforms should handle unknown type values gracefully by falling back to a generic page renderer.
Changelog
| Date | Change |
|---|---|
| 2026-06-03 | Corrected type_key rule (underscores, not hyphens) and used datetime (not date) in examples; documented {value,label} select options; corrected the Content Delivery response shape (id/type, values under metadata.custom_fields via include=metadata). |
| 2026-06-01 | Documented the repeater field type. |
| 2026-03-24 | Added required permissions documentation. |
| 2026-03-24 | Initial publication. |