Appearance
JavaScript Examples
This guide provides vanilla JavaScript/TypeScript examples for integrating with the EngageHQ API.
API Client Setup
Basic Client
javascript
const API_BASE = 'https://cdn.engagehq.retailsuccessplatform.com/api/v1/public';
class EngageHQClient {
/**
* @param {string} token - JWT token from Identity service
* @param {string} organizationId - Organization or location hashkey (org_xxx).
* Use a store location hashkey for location-specific content with distribution.
*/
constructor(token, organizationId) {
this.token = token;
this.organizationId = organizationId;
}
async request(endpoint, options = {}) {
const url = `${API_BASE}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.token}`,
'X-Organization-Context': this.organizationId,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'API request failed');
}
return response.json();
}
// Content methods
async getContent(params = {}) {
const query = new URLSearchParams(params).toString();
return this.request(`/content${query ? `?${query}` : ''}`);
}
async getContentBySlug(slug) {
return this.request(`/content/${slug}`);
}
// Circular methods
async getCirculars(params = {}) {
const query = new URLSearchParams(params).toString();
return this.request(`/circulars${query ? `?${query}` : ''}`);
}
async getCircular(id) {
return this.request(`/circulars/${id}`);
}
// Offer methods
async getOffers(params = {}) {
const query = new URLSearchParams(params).toString();
return this.request(`/offers${query ? `?${query}` : ''}`);
}
async getOffer(id) {
return this.request(`/offers/${id}`);
}
}
// Usage — pass your store location's org hashkey for location-scoped content
const client = new EngageHQClient('your-jwt-token', 'org_00000a1B2c3D4e5');TypeScript Client with Types
typescript
// types.ts
interface ContentItem {
id: string;
type: string;
slug: string;
title: string;
description: string | null;
meta_title: string | null;
meta_description: string | null;
tags: string[];
category: string | null;
featured_image: {
url: string;
alt: string;
} | null;
published_at: string;
is_featured: boolean;
}
interface ContentDetail extends ContentItem {
content: ContentBlock[];
metadata: Record<string, unknown>;
canonical_url: string | null;
og_data: OGData | null;
media_ids: string[];
}
interface ContentBlock {
type: 'heading' | 'paragraph' | 'image' | 'list' | 'quote' | 'embed';
[key: string]: unknown;
}
interface OGData {
title: string;
description: string;
image: string;
type?: string;
}
interface Circular {
id: string;
name: string;
description: string | null;
status: 'ready' | 'in_use';
total_pages: number;
format: 'tabloid' | 'letter' | 'custom';
dimensions: { width: number; height: number };
thumbnail_url: string | null;
last_used_at: string | null;
created_at: string;
}
interface Offer {
id: string;
name: string;
description: string | null;
type: 'percentage' | 'fixed' | 'bogo';
discount: DiscountInfo;
valid_from: string | null;
valid_until: string | null;
}
interface DiscountInfo {
type: 'percentage' | 'fixed' | 'bogo';
value?: number;
buy?: number;
get?: number;
display: string;
}
interface PaginatedResponse<T> {
success: boolean;
message: string;
data: T[];
meta: {
current_page: number;
last_page: number;
per_page: number;
total: number;
};
}
interface SingleResponse<T> {
success: boolean;
message: string;
data: T;
}Fetching Content
List Content with Filters
javascript
async function getArticles(client, page = 1) {
try {
const response = await client.getContent({
type: 'article',
per_page: 10,
page: page,
});
console.log(`Found ${response.meta.total} articles`);
console.log(`Page ${response.meta.current_page} of ${response.meta.last_page}`);
return response.data;
} catch (error) {
console.error('Failed to fetch articles:', error.message);
throw error;
}
}Get Featured Content
javascript
async function getFeaturedContent(client, type = null) {
const params = { featured: true, per_page: 5 };
if (type) params.type = type;
const { data } = await client.getContent(params);
return data;
}
// Usage
const featuredArticles = await getFeaturedContent(client, 'article');
const allFeatured = await getFeaturedContent(client);Filter by Tags
javascript
async function getContentByTags(client, tags) {
// Tags use OR logic - matches any tag
const params = new URLSearchParams();
tags.forEach(tag => params.append('tags[]', tag));
const { data } = await client.request(`/content?${params}`);
return data;
}
// Usage
const summerContent = await getContentByTags(client, ['summer', 'sale']);Get Single Content Item
javascript
async function renderArticle(client, slug) {
try {
const { data: article } = await client.getContentBySlug(slug);
// Render the article
document.title = article.meta_title || article.title;
const container = document.getElementById('article');
container.innerHTML = `
<article>
<h1>${article.title}</h1>
${article.featured_image ? `
<img src="${article.featured_image.url}"
alt="${article.featured_image.alt}" />
` : ''}
<div class="content">
${renderContentBlocks(article.content)}
</div>
</article>
`;
} catch (error) {
if (error.message === 'Content not found') {
// Handle 404
document.getElementById('article').innerHTML = '<p>Article not found</p>';
} else {
throw error;
}
}
}Rendering Content Blocks
javascript
function renderContentBlocks(blocks) {
if (!blocks || !Array.isArray(blocks)) return '';
return blocks.map(block => {
switch (block.type) {
case 'heading':
return `<h${block.level}>${escapeHtml(block.text)}</h${block.level}>`;
case 'paragraph':
return `<p>${block.text}</p>`; // HTML allowed in paragraphs
case 'image':
return `
<figure>
<img src="${block.url}" alt="${escapeHtml(block.alt || '')}"
${block.width ? `width="${block.width}"` : ''}
${block.height ? `height="${block.height}"` : ''} />
${block.caption ? `<figcaption>${escapeHtml(block.caption)}</figcaption>` : ''}
</figure>
`;
case 'list':
const tag = block.ordered ? 'ol' : 'ul';
const items = block.items.map(item => `<li>${escapeHtml(item)}</li>`).join('');
return `<${tag}>${items}</${tag}>`;
case 'quote':
return `
<blockquote>
<p>${escapeHtml(block.text)}</p>
${block.attribution ? `<cite>— ${escapeHtml(block.attribution)}</cite>` : ''}
</blockquote>
`;
case 'embed':
return renderEmbed(block);
default:
console.warn(`Unknown block type: ${block.type}`);
return '';
}
}).join('\n');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function renderEmbed(block) {
switch (block.embed_type) {
case 'video':
// Extract video ID for YouTube/Vimeo embeds
if (block.url.includes('youtube.com') || block.url.includes('youtu.be')) {
const videoId = extractYouTubeId(block.url);
return `
<div class="video-embed">
<iframe src="https://www.youtube.com/embed/${videoId}"
frameborder="0" allowfullscreen></iframe>
</div>
`;
}
return `<a href="${block.url}">${block.title || 'Watch video'}</a>`;
default:
return `<a href="${block.url}">${block.title || 'View content'}</a>`;
}
}
function extractYouTubeId(url) {
const match = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/))([^&?]+)/);
return match ? match[1] : '';
}Working with Circulars
Display Circular Thumbnails
javascript
async function displayCircularThumbnails(client) {
const { data: circulars } = await client.getCirculars({ per_page: 10 });
const container = document.getElementById('circulars');
container.innerHTML = circulars.map(circular => `
<div class="circular-card" data-id="${circular.id}">
<img src="${circular.thumbnail_url}" alt="${circular.name}" />
<h3>${circular.name}</h3>
<p>${circular.total_pages} pages • ${circular.format}</p>
</div>
`).join('');
// Add click handlers
container.querySelectorAll('.circular-card').forEach(card => {
card.addEventListener('click', () => {
openCircularViewer(card.dataset.id);
});
});
}Render Circular Pages
javascript
async function openCircularViewer(client, circularId) {
const { data: circular } = await client.getCircular(circularId);
const viewer = document.getElementById('circular-viewer');
viewer.innerHTML = `
<div class="circular-header">
<h2>${circular.name}</h2>
<span>Page <span id="current-page">1</span> of ${circular.total_pages}</span>
</div>
<div class="circular-pages" id="pages-container"></div>
<div class="circular-nav">
<button id="prev-page" disabled>Previous</button>
<button id="next-page">Next</button>
</div>
`;
// Render first page
let currentPage = 1;
renderCircularPage(circular.layout_data.pages[0]);
// Navigation
document.getElementById('prev-page').addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
renderCircularPage(circular.layout_data.pages[currentPage - 1]);
updateNavigation();
}
});
document.getElementById('next-page').addEventListener('click', () => {
if (currentPage < circular.total_pages) {
currentPage++;
renderCircularPage(circular.layout_data.pages[currentPage - 1]);
updateNavigation();
}
});
function updateNavigation() {
document.getElementById('current-page').textContent = currentPage;
document.getElementById('prev-page').disabled = currentPage === 1;
document.getElementById('next-page').disabled = currentPage === circular.total_pages;
}
}
function renderCircularPage(page) {
const container = document.getElementById('pages-container');
container.innerHTML = page.regions.map(region => `
<div class="region" style="
position: absolute;
left: ${region.position.x}px;
top: ${region.position.y}px;
width: ${region.dimensions.width}px;
height: ${region.dimensions.height}px;
">
${renderRegionContent(region.content)}
</div>
`).join('');
}Displaying Offers
Active Offers List
javascript
async function displayActiveOffers(client) {
const { data: offers } = await client.getOffers();
const container = document.getElementById('offers');
container.innerHTML = offers.map(offer => `
<div class="offer-card offer-${offer.type}">
<div class="offer-badge">${offer.discount.display}</div>
<h3>${offer.name}</h3>
<p>${offer.description || ''}</p>
${offer.valid_until ? `
<p class="offer-expires">
Expires: ${formatDate(offer.valid_until)}
</p>
` : ''}
</div>
`).join('');
}
function formatDate(isoString) {
return new Date(isoString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}Countdown Timer for Expiring Offers
javascript
function displayOfferWithCountdown(offer) {
if (!offer.valid_until) return displayOffer(offer);
const expiresAt = new Date(offer.valid_until);
function updateCountdown() {
const now = new Date();
const diff = expiresAt - now;
if (diff <= 0) {
container.innerHTML = '<p class="expired">This offer has expired</p>';
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
document.getElementById(`countdown-${offer.id}`).textContent =
`${days}d ${hours}h ${minutes}m remaining`;
}
// Update every minute
updateCountdown();
setInterval(updateCountdown, 60000);
}Pagination Helper
javascript
async function* paginateAll(client, endpoint, params = {}) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await client.request(
`${endpoint}?${new URLSearchParams({ ...params, page })}`
);
for (const item of response.data) {
yield item;
}
hasMore = page < response.meta.last_page;
page++;
}
}
// Usage
async function getAllArticles(client) {
const articles = [];
for await (const article of paginateAll(client, '/content', { type: 'article' })) {
articles.push(article);
}
return articles;
}Platform Elements
When rendering content sections, the content array may include platform_element entries that represent components owned by your platform. These are resolved client-side by mapping platform_key and element_key to local components.
Component Registry
javascript
// Register your platform's components
const platformComponents = {
'ecom360:popular-sale-items': renderPopularSaleItems,
'ecom360:weekly-ad-banner': renderWeeklyAdBanner,
'ecom360:store-locator-map': renderStoreLocatorMap,
};
function renderFallback(section) {
console.warn(`Unknown platform element: ${section.platform_key}:${section.element_key}`);
return '';
}Section Resolver
javascript
function resolveSection(section) {
if (section.type === 'platform_element') {
const key = `${section.platform_key}:${section.element_key}`;
const renderer = platformComponents[key] || renderFallback;
return renderer(section);
}
// Fall back to built-in section rendering
return renderBuiltInSection(section);
}
function renderSections(sections) {
if (!sections || !Array.isArray(sections)) return '';
return sections.map(section => {
const html = resolveSection(section);
const padding = section.settings?.padding || 'medium';
const anchor = section.settings?.anchor_id || '';
return `
<div class="section section--${padding}" ${anchor ? `id="${anchor}"` : ''}>
${html}
</div>
`;
}).join('\n');
}Example Component
javascript
function renderPopularSaleItems(section) {
const { max_items = 12, layout = 'carousel' } = section.config || {};
// Fetch and render your platform-specific data
return `
<div class="popular-sale-items popular-sale-items--${layout}"
data-max-items="${max_items}">
<!-- Hydrated client-side with product data -->
</div>
`;
}See the Platform Elements reference for the full API and storage format.
Error Handling
javascript
class APIError extends Error {
constructor(message, status, errors = null) {
super(message);
this.name = 'APIError';
this.status = status;
this.errors = errors;
}
}
async function fetchWithErrorHandling(client, endpoint) {
try {
return await client.request(endpoint);
} catch (error) {
if (error.status === 401) {
// Token expired - redirect to login
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname);
return;
}
if (error.status === 404) {
// Resource not found - show friendly message
showNotification('The requested content was not found', 'warning');
return null;
}
if (error.status === 429) {
// Rate limited - wait and retry
await sleep(5000);
return fetchWithErrorHandling(client, endpoint);
}
// Unknown error - log and show generic message
console.error('API Error:', error);
showNotification('Something went wrong. Please try again.', 'error');
throw error;
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Changelog
| Date | Change |
|---|---|
| 2026-03-24 | Added platform elements component registry and resolution examples. |
| 2026-02-23 | Updated examples for CloudFront CDN and Lambda@Edge delivery. |
| 2026-01-28 | Expanded documentation. |
| 2026-01-15 | Initial publication. |