Appearance
Content API
The Content API provides access to published content items including articles, pages, recipes, hero banners, homepages, and more.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/public/content | List published content items |
| GET | /api/v1/public/content/{slug} | Get a content item by slug |
List Content
Retrieve a paginated list of published content items for your organization.
http
GET /api/v1/public/contentQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
per_page | integer | 20 | Number of items per page (1-100) |
page | integer | 1 | Page number |
type | string|array | - | Filter by content type (single or multiple) |
tags | array | - | Filter by tags (OR logic) |
category | string | - | Filter by category (max 100 chars) |
featured | boolean | - | Filter featured items only |
is_primary | boolean | - | Filter primary items only (useful for homepage) |
location_id | string | - | Location hashkey (org_xxxxx) for multi-location content distribution |
placement | string | - | Filter heroes by placement (home, page, landing). Filters on metadata.placement. |
include | string | - | Comma-separated list of additional fields to include (content, metadata) |
expand | string | - | Comma-separated list of expansion types (linked_content) |
Content Types
The type parameter accepts the following values:
| Type | Description |
|---|---|
page | Static pages |
article | Blog posts, news articles, and informational content |
recipe | Recipes with ingredients and instructions |
menu | Menu pages |
hero | Hero banner content |
landing | Landing pages |
homepage | Primary site homepages (supports multiple variants) |
circular | Digital circular content |
Custom types
The type parameter also accepts any organization-defined custom type type_key (see Custom Types), not just the built-in values above. A custom type's field values are returned under metadata.custom_fields — request them with include=metadata on this list endpoint (the /{slug} endpoint always includes metadata).
Type Filtering
The type parameter supports both single values and arrays for filtering multiple content types at once.
Single type (string):
http
GET /api/v1/public/content?type=articleMultiple types (array):
http
GET /api/v1/public/content?type[]=article&type[]=recipe&type[]=pageReturns content matching any of the specified types (OR logic).
Tags Filtering
Tags use OR logic - content matching any provided tag will be returned:
http
GET /api/v1/public/content?tags[]=summer&tags[]=saleReturns content tagged with "summer" OR "sale".
Homepage Filtering
To retrieve the active (primary) homepage:
http
GET /api/v1/public/content?type=homepage&is_primary=trueOrganizations can have multiple homepage variants (seasonal, A/B tests), but only one can be marked as primary at a time. Use is_primary=true to fetch the currently active homepage.
Hero Filtering by Placement
Heroes can be filtered server-side by their intended placement using the placement query parameter. This allows you to distinguish between heroes created for the home page carousel versus heroes intended for other content pages.
Available placement values:
| Value | Description |
|---|---|
home | Heroes intended for the home page carousel |
page | Heroes for general content pages |
landing | Heroes for landing pages |
Get all home page heroes:
http
GET /api/v1/public/content?type=hero&placement=home&include=contentExample: Fetching home page hero carousel
javascript
const response = await fetch(
'https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content?type=hero&placement=home&include=content',
{
headers: {
'Authorization': `Bearer ${token}`,
},
}
);
const { data } = await response.json();
// Get slides from all home heroes for carousel
const carouselSlides = data.flatMap(hero => hero.content?.slides || []);Default Placement
Heroes created without an explicit placement default to home. This ensures backward compatibility with existing hero content.
Selective Field Expansion
By default, the list endpoint returns minimal fields optimized for listings. Use the include parameter to request additional fields without making a second API call.
Available include values:
content- The full content blocks (useful for menus, banners)metadata- Additional metadata (author, reading time, etc.)
Include content only:
http
GET /api/v1/public/content?type=menu&include=contentInclude metadata only:
http
GET /api/v1/public/content?type=article&include=metadataInclude both fields:
http
GET /api/v1/public/content?type=menu&include=content,metadataThis is particularly useful for content types like menus where you need the full content structure but want to avoid a second API call for each item.
CDN Caching
Requests with different include values are cached separately by the CDN via CloudFront cache policies. All variations are automatically invalidated when content is published.
Linked Content Expansion
Content sections can reference other content items (e.g., a hero section linking to a reusable hero content item). Use the expand parameter to resolve these linked references inline, eliminating the need for additional API calls.
Available expand values:
linked_content- Resolves linked content references within sections
Expand linked content:
http
GET /api/v1/public/content?type=page&include=content&expand=linked_contentRequires include=content
The expand=linked_content parameter only works when include=content is also specified, as linked content exists within the content structure.
When a page contains a hero section that links to a separate hero content item:
Without expansion (default):
json
{
"content": {
"sections": [{
"type": "hero",
"content": {
"source_mode": "linked",
"linked_hero_id": "con_00000a1B2c3D4e5"
}
}]
}
}With expand=linked_content:
json
{
"content": {
"sections": [{
"type": "hero",
"content": {
"source_mode": "linked",
"linked_hero_id": "con_00000a1B2c3D4e5",
"_linked_content": {
"id": "con_00000a1B2c3D4e5",
"title": "Summer Sale Hero",
"type": "hero",
"status": "published",
"content": {
"slides": [
{
"headline": "Summer Sale",
"subheadline": "Up to 50% off",
"background_image": "https://cdn.example.com/hero-bg.jpg"
}
]
},
"published_at": "2024-06-01T00:00:00+00:00"
},
"slides": [
{
"headline": "Summer Sale",
"subheadline": "Up to 50% off",
"background_image": "https://cdn.example.com/hero-bg.jpg"
}
]
}
}]
}
}Expanded Fields
| Field | Type | Description |
|---|---|---|
_linked_content | object | Full linked content item data |
_linked_content.id | string | Linked content hashkey |
_linked_content.title | string | Linked content title |
_linked_content.type | string | Content type of linked item |
_linked_content.status | string | Publication status |
_linked_content.content | object | Full content structure |
_linked_content.published_at | string | ISO 8601 publish timestamp |
slides | array | Convenience field with slides array merged directly into content (hero sections only) |
Error Handling
If a linked content reference cannot be resolved, an error indicator is included:
json
{
"content": {
"sections": [{
"type": "hero",
"content": {
"source_mode": "linked",
"linked_hero_id": "con_00000invalid",
"_expansion_error": "not_found"
}
}]
}
}| Error | Description |
|---|---|
not_found | Linked content item doesn't exist or isn't published |
circular_reference | Circular reference detected (content links to itself) |
fetch_error | Server error while fetching linked content |
Performance
Use expand=linked_content judiciously. Each linked reference requires an additional database query. For pages with many linked sections, consider caching the expanded response client-side.
Example Request
javascript
const params = new URLSearchParams({
type: 'article',
featured: 'true',
per_page: '10',
});
const response = await fetch(
`https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content?${params}`,
{
headers: {
'Authorization': `Bearer ${token}`,
},
}
);Response
json
{
"success": true,
"message": "Content retrieved",
"data": [
{
"id": "con_00000k1L2m3N4o5",
"type": "article",
"slug": "summer-sale-announcement",
"title": "Summer Sale Announcement",
"description": "Get ready for our biggest summer sale event of the year...",
"meta_title": "Summer Sale 2024 | Acme Grocery",
"meta_description": "Shop our summer sale with discounts up to 50%",
"tags": ["summer", "sale", "promotions"],
"category": "promotions",
"media_ids": ["med_00000x1Y2z3A4b5"],
"published_at": "2024-06-01T00:00:00+00:00",
"is_featured": true,
"is_primary": false
}
],
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 10,
"total": 28,
"links": [
{ "url": null, "label": "« Previous", "active": false },
{ "url": "...", "label": "1", "active": true },
{ "url": "...", "label": "2", "active": false },
{ "url": "...", "label": "Next »", "active": false }
]
}
}Response Fields (List)
| Field | Type | Description |
|---|---|---|
id | string | Unique content identifier (hashkey) |
type | string | Content type |
slug | string | URL-friendly identifier |
title | string | Display title |
description | string | Short description/excerpt |
meta_title | string | SEO meta title |
meta_description | string | SEO meta description |
tags | array | Array of tag strings |
category | string | Content category |
media_ids | array | Array of associated media hashkeys |
published_at | string | ISO 8601 publish timestamp |
is_featured | boolean | Whether content is featured |
is_primary | boolean | Whether this is the primary content for its type (e.g., active homepage) |
Optional Fields (via include parameter)
These fields are only returned when explicitly requested using the include query parameter:
| Field | Include Value | Type | Description |
|---|---|---|---|
content | content | array | Full content blocks (see Schemas) |
metadata | metadata | object | Type-specific metadata (see below) |
Metadata by Content Type
When include=metadata is specified, the metadata object contains type-specific fields:
Articles (article):
| Field | Type | Description |
|---|---|---|
author | string | Author name |
excerpt | string | Brief summary for listings |
reading_time | integer | Estimated minutes to read |
Recipes (recipe):
| Field | Type | Description |
|---|---|---|
prep_time | string | Preparation time in minutes |
cook_time | string | Cooking time in minutes |
servings | string | Number of servings |
difficulty | string | Difficulty level (easy, medium, hard) |
cuisine | string | Type of cuisine |
diet_types | array | Dietary categories (vegetarian, gluten-free, etc.) |
description | string | Recipe description |
Heroes (hero):
| Field | Type | Description |
|---|---|---|
placement | string | Where the hero is used: home, page, or landing |
height | string | Display height: full, large, medium, small |
text_align | string | Text alignment: left, center, right |
text_color | string | Text color scheme: white, black, auto |
animation | string | Entry animation: none, fade, slide-up, slide-down, zoom |
autoplay | boolean | Auto-advance slides in carousel mode (default: true) |
autoplay_interval | integer | Milliseconds between slides (default: 5000) |
show_arrows | boolean | Show navigation arrows (default: true) |
show_dots | boolean | Show navigation dots (default: true) |
pause_on_hover | boolean | Pause autoplay on hover (default: true) |
Menus (menu):
| Field | Type | Description |
|---|---|---|
menu_type | string | Menu placement: header or footer |
Custom types:
For organization-defined custom types, metadata contains a custom_fields object keyed by the field keys defined in the type's schema:
| Field | Type | Description |
|---|---|---|
custom_fields | object | Custom field values, keyed by field key. A repeater field's value is an array of objects (one per row). Booleans are returned as real JSON booleans. |
Sort Order
Results are sorted by:
- Featured items first (
is_featureddescending) - Sort order (
sort_orderascending) - Publication date (
published_atdescending)
Caching
| Cache | TTL |
|---|---|
| Browser (max-age) | 60 seconds |
| CDN (CloudFront policy) | Up to 24 hours (invalidated on publish) |
| Stale-while-revalidate | 60 seconds |
CDN caching is managed by CloudFront cache policies, not the Cache-Control response header. The browser max-age controls client-side caching.
Get Content by Slug
Retrieve a single content item by its URL slug.
http
GET /api/v1/public/content/{slug}Path Parameters
| Parameter | Type | Description |
|---|---|---|
slug | string | The URL-friendly slug of the content |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
location_id | string | - | Location hashkey (org_xxxxx) for org-hierarchy resolution |
expand | string | - | Comma-separated list of expansion types (linked_content) |
Hierarchy Resolution with location_id
When location_id is provided, the endpoint walks the organization hierarchy to find the most specific content match for the given slug. The resolution order is:
- Store-level content (highest priority)
- Retailer-level content
- Wholesaler-level content (fallback default)
This allows a wholesaler to publish default content (e.g., a generic privacy policy), while individual retailers or stores can override it with their own version using the same slug. Ancestor content is only visible if its distribution_mode is set to all_descendants or custom.
Without location_id
When no location_id is provided, only content belonging to the authenticated organization is searched. The hierarchy walk requires a location_id to determine the ancestry chain.
Example Request
javascript
const response = await fetch(
'https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content/summer-sale-announcement',
{
headers: {
'Authorization': `Bearer ${token}`,
},
}
);With hierarchy resolution (multi-location):
javascript
const response = await fetch(
'https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content/privacy-policy?location_id=org_store123',
{
headers: {
'Authorization': `Bearer ${token}`,
},
}
);In this example, if org_store123 is a store, the API checks the store first for content with the slug privacy-policy, then its parent retailer, then the wholesaler -- returning the first match found. Ancestor content must have a distribution_mode of all_descendants or custom to be returned.
With linked content expansion:
javascript
const response = await fetch(
'https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content/homepage-summer?expand=linked_content',
{
headers: {
'Authorization': `Bearer ${token}`,
},
}
);Response
json
{
"success": true,
"message": "Content retrieved",
"data": {
"id": "con_00000k1L2m3N4o5",
"type": "article",
"slug": "summer-sale-announcement",
"title": "Summer Sale Announcement",
"description": "Get ready for our biggest summer sale...",
"meta_title": "Summer Sale 2024 | Acme Grocery",
"meta_description": "Shop our summer sale with discounts up to 50%",
"tags": ["summer", "sale", "promotions"],
"category": "promotions",
"media_ids": ["med_00000x1Y2z3A4b5", "med_00000m1N2o3P4q5"],
"published_at": "2024-06-01T00:00:00+00:00",
"is_featured": true,
"is_primary": false,
"content": [
{
"type": "heading",
"text": "Our Biggest Sale Ever",
"level": 2
},
{
"type": "paragraph",
"text": "This summer, we're bringing you incredible savings..."
},
{
"type": "image",
"url": "https://cdn.example.com/images/sale-items.jpg",
"alt": "Featured sale items",
"caption": "Just a few of our amazing deals"
}
],
"metadata": {
"author": "Marketing Team",
"reading_time": 5,
"custom_field": "value"
},
"canonical_url": "https://acmegrocery.com/blog/summer-sale-announcement",
"og_data": {
"title": "Summer Sale Announcement",
"description": "Don't miss our biggest summer sale",
"image": "https://cdn.example.com/images/og-summer-sale.jpg"
}
}
}Additional Response Fields (Detail)
The detail response includes all list fields plus:
| Field | Type | Description |
|---|---|---|
content | array | Array of content blocks (see Schemas). May include platform_element entries from consuming platforms (see Platform Elements). |
metadata | object | Custom metadata key-value pairs |
canonical_url | string | Canonical URL for SEO |
og_data | object | Open Graph data for social sharing |
og_data.title | string | OG title |
og_data.description | string | OG description |
og_data.image | string | OG image URL |
media_ids | array | Array of associated media hashkeys |
Caching
| Cache | TTL |
|---|---|
| Browser (max-age) | 300 seconds |
| CDN (CloudFront policy) | Up to 24 hours (invalidated on publish) |
| Stale-while-revalidate | 300 seconds |
CDN caching is managed by CloudFront cache policies, not the Cache-Control response header. The browser max-age controls client-side caching.
Preview Mode
When a request includes a valid preview token, preview session, or signed preview URL, the API enters preview mode. This affects the response in two ways:
Additional Fields in List Responses
In preview mode, each content item in the list response includes two extra fields:
| Field | Type | Description |
|---|---|---|
status | string | Content status (published, draft, scheduled) |
expires_at | string | ISO 8601 expiration timestamp (if set) |
Preview Metadata
A preview object is appended to the response envelope:
List endpoint:
json
{
"success": true,
"data": [...],
"meta": {...},
"preview": {
"is_preview": true,
"scope": "all",
"preview_at": "2024-06-01T12:00:00+00:00",
"expires_at": "2024-06-02T12:00:00+00:00"
}
}Detail endpoint:
json
{
"success": true,
"data": {...},
"preview": {
"is_preview": true,
"content_status": "draft",
"preview_at": "2024-06-01T12:00:00+00:00",
"is_visible_at_preview_time": false,
"expires_at": "2024-06-02T12:00:00+00:00"
}
}See the Authentication guide for details on generating preview tokens and sessions.
No Caching
Preview mode responses are never cached. The Cache-Control header is set to private, no-store, no-cache, must-revalidate and an X-Preview-Mode: true header is added.
Error Responses
400 Bad Request
json
{
"success": false,
"message": "Organization context required",
"errors": null
}Ensure your request includes proper authentication with organization access.
404 Not Found
json
{
"success": false,
"message": "Content not found",
"errors": null
}The content either:
- Doesn't exist
- Is not published
- Has expired
- Belongs to a different organization
Changelog
| Date | Change |
|---|---|
| 2026-06-03 | Documented custom-type type_key filtering and custom field values under metadata.custom_fields. |
| 2026-03-24 | Added platform elements documentation to content responses. |
| 2026-03-24 | Added custom types support for user-defined content types. |
| 2026-03-08 | Corrected content API documentation and added placement filter. |
| 2026-02-28 | Removed content_key; added circular-campaign integration. |
| 2026-02-24 | Added content_key resolution documentation. |
| 2026-02-23 | Updated for CloudFront CDN delivery. |
| 2026-02-11 | Added hero placement documentation. |
| 2026-02-09 | Added selective include parameter. |
| 2026-02-05 | Consolidated content type documentation. |
| 2026-02-04 | Added section settings documentation. |
| 2026-01-28 | Expanded documentation. |
| 2026-01-15 | Initial publication. |