Appearance
Caching Strategy
The EngageHQ Public API is designed for high-volume traffic with aggressive CDN caching and automatic invalidation. Content is served from edge locations worldwide and refreshed within seconds of being published.
Cache Architecture
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ ┌──────────┐
│ Browser │────▶│ CDN (ours) │────▶│ Vapor CF │────▶│ API Server │────▶│ Database │
│ Cache │◀────│ cdn.engagehq.* │◀────│ api.engagehq.* │◀────│ Cache │◀────│ │
└─────────────┘ └─────────────────┘ └─────────────────┘ └─────────────┘ └──────────┘
max-age 24h + invalidation pass-through Redis/Memory MySQLHow It Works
CDN layer (
cdn.engagehq.retailsuccessplatform.com) — caches all public content for 24 hours at edge locations. When content is published, updated, or deleted, the CDN cache is automatically invalidated and the next request fetches fresh data from the origin.Browser layer — short TTLs via
max-agecontrol how long the user's browser caches responses before checking the CDN again.Stale-while-revalidate — allows browsers to serve cached content immediately while refreshing in the background, providing instant page loads.
Always use the CDN endpoint
For public content delivery, use cdn.engagehq.retailsuccessplatform.com instead of the direct API endpoint. The CDN provides lower latency, higher availability, and automatic cache management.
Cache-Control Headers
Every response includes Cache-Control headers that control browser caching:
http
Cache-Control: public, max-age=300, s-maxage=0, stale-while-revalidate=300Header Components
| Directive | Purpose | Who Uses It |
|---|---|---|
max-age | Browser cache duration | End user's browser |
s-maxage=0 | Disables intermediate proxy caching | Internal infrastructure |
stale-while-revalidate | Grace period for async refresh | Browser |
CDN caching is separate
The CDN does not use Cache-Control headers for its caching decisions. It uses a policy-based 24-hour cache with on-demand invalidation. The s-maxage=0 directive is an internal infrastructure detail — you don't need to worry about it.
Stale-While-Revalidate (SWR)
During the SWR window, the browser serves cached content immediately while refreshing in the background:
Timeline (content detail endpoint, max-age=300, swr=300):
├── 0-300s: Fresh from browser cache (instant)
├── 300-600s: Stale but served instantly, refresh happens in background
└── 600s+: Must wait for fresh response from CDNThis means users almost never see a loading spinner for content — the browser either has fresh data or serves stale data while fetching an update.
Cache TTLs by Endpoint
| Endpoint | Browser | SWR | CDN | Refreshed |
|---|---|---|---|---|
GET /content | 60s | 60s | 24h | On publish |
GET /content/{slug} | 300s | 300s | 24h | On publish |
GET /circulars | 60s | 300s | 24h | On publish |
GET /circulars/{id} | 300s | 300s | 24h | On publish |
GET /offers | 0s | 30s | 24h | On publish |
GET /offers/{id} | 0s | 30s | 24h | On publish |
Why Different Browser TTLs?
- Content detail (300s) — individual pages change infrequently; a 5-minute browser cache reduces repeat requests while the CDN ensures freshness after edits
- Content listing (60s) — lists may include newly published items; shorter browser TTL ensures new content appears within a minute
- Offers (0s) — time-sensitive validity rules mean browsers should always check the CDN for the latest data
CDN Cache Behavior
All public endpoints share the same CDN behavior:
- 24-hour cache at edge locations worldwide
- Automatic invalidation when content is published, updated, or deleted
- Per-location isolation — different locations/organizations get separate cache entries via
X-Organization-Context - Query string aware — different filter combinations are cached independently
ETag Support
All responses include an ETag header for conditional requests:
http
ETag: "a1b2c3d4e5f6"Using ETags
Make conditional requests with If-None-Match:
javascript
const response = await fetch('https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content/summer-sale', {
headers: {
'Authorization': `Bearer ${token}`,
'X-Organization-Context': organizationId,
'If-None-Match': '"a1b2c3d4e5f6"' // Previous ETag
}
});
if (response.status === 304) {
// Content hasn't changed, use cached version
console.log('Using cached content');
} else {
// Content changed, use new response
const newETag = response.headers.get('ETag');
const data = await response.json();
}Vary Header
Responses include a Vary header indicating which request headers affect caching:
http
Vary: X-Organization-ContextThe CDN uses X-Organization-Context (not Authorization) as the cache key for per-location isolation. This means different JWT tokens for the same location share cache entries — token rotation doesn't bust the cache.
Always include X-Organization-Context
The X-Organization-Context header is required when using the CDN endpoint. Set it to the store/location org hashkey you want content for. Without it, responses may not be cached correctly. See the Authentication guide for details on location resolution.
Cache Invalidation
When content is updated in EngageHQ, caches are automatically invalidated:
- Redis cache is cleared immediately
- CDN invalidation is triggered for affected paths — fresh content is served within seconds
- ETag changes force clients to refetch on their next request
What Gets Invalidated
| Action | Invalidated Paths |
|---|---|
| Content update | /api/v1/public/content*, /api/v1/public/content/{slug} |
| Slug change | Both old and new slug paths |
| Content delete | Affected content paths |
| Circular update | /api/v1/public/circulars*, /api/v1/public/circulars/{id} |
| Offer update | /api/v1/public/offers* |
No action needed from API consumers
Cache invalidation is fully automatic. When an editor publishes a change, the CDN serves the updated content within seconds. You don't need to implement any cache-busting logic in your application.
Client-Side Caching Recommendations
For Content and Circulars
The CDN handles content freshness, so client-side caching can focus on UX. Use short TTLs to avoid unnecessary requests while relying on the CDN for the latest data:
javascript
class ContentCache {
constructor() {
this.cache = new Map();
}
async getContent(slug, token, orgId) {
const cacheKey = `content:${slug}`;
const cached = this.cache.get(cacheKey);
// Use cached if less than 60 seconds old
if (cached && Date.now() - cached.timestamp < 60000) {
return cached.data;
}
const response = await fetch(
`https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content/${slug}`,
{ headers: { 'Authorization': `Bearer ${token}`, 'X-Organization-Context': orgId } }
);
const data = await response.json();
this.cache.set(cacheKey, {
data: data.data,
timestamp: Date.now()
});
return data.data;
}
}For Offers
Do not cache offers locally — they can expire or reach usage limits at any time:
javascript
// Good - always fetch fresh from CDN
async function getOffers(token, orgId) {
const response = await fetch(
'https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/offers',
{ headers: { 'Authorization': `Bearer ${token}`, 'X-Organization-Context': orgId } }
);
return response.json();
}
// Bad - caching offers can show expired promotions
const cachedOffers = localStorage.getItem('offers'); // DON'T DO THISReact Query / SWR Integration
React Query
javascript
import { useQuery } from '@tanstack/react-query';
function useContent(slug) {
return useQuery({
queryKey: ['content', slug],
queryFn: () => fetchContent(slug),
staleTime: 60 * 1000, // Consider fresh for 60s
cacheTime: 5 * 60 * 1000, // Keep in cache for 5min
});
}
function useOffers() {
return useQuery({
queryKey: ['offers'],
queryFn: fetchOffers,
staleTime: 0, // Always refetch offers
refetchOnWindowFocus: true, // Refresh when user returns
});
}SWR
javascript
import useSWR from 'swr';
function useContent(slug) {
return useSWR(`/content/${slug}`, fetcher, {
revalidateOnFocus: false,
dedupingInterval: 60000, // Dedupe requests for 60s
});
}
function useOffers() {
return useSWR('/offers', fetcher, {
revalidateOnFocus: true, // Always refetch on focus
refreshInterval: 60000, // Auto-refresh every 60s
});
}Performance Best Practices
1. Batch Related Requests
Fetch content lists instead of individual items when possible:
javascript
// Good - one request for multiple items
const { data } = await fetch(
'https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content?type=article&per_page=10'
);
// Bad - multiple requests for same data
for (const slug of slugs) {
await fetch(`https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content/${slug}`);
}2. Use Appropriate Page Sizes
Request only what you need:
javascript
// Homepage hero - just need 3 items
GET /content?type=hero&featured=true&per_page=3
// Full listing - use pagination
GET /content?type=article&per_page=20&page=13. Preload Critical Content
Preload content that users will likely need:
javascript
// In your router or page component
function prefetchRelated(contentType) {
// Warm the cache for likely next pages
queryClient.prefetchQuery(['content', contentType], () =>
fetchContent({ type: contentType, per_page: 5 })
);
}4. Handle Stale Data Gracefully
Show cached data immediately, then update when fresh data arrives:
javascript
function ContentDisplay({ slug }) {
const { data, isValidating } = useSWR(`/content/${slug}`);
return (
<div>
{data && <Article content={data} />}
{isValidating && <RefreshIndicator />}
</div>
);
}Debugging Cache Issues
Check Response Headers
bash
curl -I "https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content/my-slug" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Organization-Context: $ORG_ID"Look for:
Cache-Control— browser caching directives (max-age,stale-while-revalidate)ETag— content hash for conditional requestsX-Cache— CDN cache status (Hit from cloudfrontorMiss from cloudfront)Age— seconds since the CDN cached this response (resets to 0 after invalidation)
Understanding the Age Header
The Age header tells you how long ago the CDN cached the response:
Age: 0 → Just fetched from origin (after invalidation or first request)
Age: 3600 → Cached 1 hour ago
Age: 86400 → Cached 24 hours ago (will be refreshed on next request)After content is published, Age resets to 0 because the CDN invalidation forces a fresh fetch from the origin.
Force Cache Bypass
Add a cache-busting parameter for debugging (not for production):
javascript
// Development only
const response = await fetch(
`https://cdn.engagehq.retailsuccessplatform.com/api/v1/public/content?_=${Date.now()}`
);Changelog
| Date | Change |
|---|---|
| 2026-02-23 | Added CloudFront CDN caching and Lambda@Edge integration guidance. |
| 2026-01-28 | Expanded documentation. |
| 2026-01-15 | Initial publication. |