02 — Content Delivery API
Dev Guide — Savoy Signature Hotels
PRD refs:08_API_Contracts.md,06_Content_Modeling_Umbraco.md,10_MultiLanguage_and_i18n.md
1. Overview
Section titled “1. Overview”Umbraco 17 ships with the Content Delivery API v2, a read-only REST API that exposes published content as JSON. This is the primary data source for the Next.js frontend. This guide covers endpoint usage, headers, filtering, pagination, and the FE client contract.
2. Base URL
Section titled “2. Base URL”{UMBRACO_API_URL}/umbraco/delivery/api/v2| Environment | Base URL |
|---|---|
| DEV | https://savoy.dev-cms.wycreative.com/umbraco/delivery/api/v2 |
| STAGE | https://savoy.stage-cms.wycreative.com/umbraco/delivery/api/v2 |
| QA | https://qa-cms.savoysignature.com/umbraco/delivery/api/v2 |
| PROD | https://cms.savoysignature.com/umbraco/delivery/api/v2 |
3. Authentication & CORS
Section titled “3. Authentication & CORS”- Authentication: None — the API is public (read-only published content)
- API Key: Optional, set via
UMBRACO_API_KEYenv var for rate-limiting/tracking - CORS: Configured to accept requests from Next.js origin only
4. Required Request Headers
Section titled “4. Required Request Headers”Every API request MUST include these headers:
const headers = { 'Accept-Language': locale, // 'pt' or 'en' 'Start-Item': siteKey, // e.g., 'savoy-palace' 'Api-Key': process.env.UMBRACO_API_KEY, // optional};Critical: The Start-Item header scopes all queries to the requesting site’s content tree. Without it, queries may return content from other hotel sites.
Accept-Language determines which language variant is returned. If the requested locale is not published for a content item, the API returns 404.
5. Core Endpoints
Section titled “5. Core Endpoints”5.1 Fetch by Route Path
Section titled “5.1 Fetch by Route Path”GET /content/item/{path}Primary endpoint for page rendering. The {path} is the URL slug (e.g., /accommodation/deluxe-room).
Example:
GET /umbraco/delivery/api/v2/content/item/accommodation/deluxe-roomAccept-Language: enStart-Item: savoy-palaceResponse shape:
{ "name": "Deluxe Room", "route": { "path": "/accommodation/deluxe-room", "startItem": { "id": "guid-here", "path": "savoy-palace" } }, "contentType": "roomDetailPage", "cultures": { "pt": { "path": "/alojamento/quarto-deluxe", "startItem": { "path": "savoy-palace" } }, "en": { "path": "/accommodation/deluxe-room", "startItem": { "path": "savoy-palace" } } }, "properties": { "roomName": "Deluxe Room", "roomCategory": "Room", "shortDescription": "...", "modules": { "items": [ { "contentType": "pageHero", "properties": { ... } }, { "contentType": "richTextBlock", "properties": { ... } } ] } }}5.2 Query / Filter Content
Section titled “5.2 Query / Filter Content”GET /contentUsed for listing pages (rooms, restaurants, news, etc.).
Query Parameters:
| Parameter | Usage | Example |
|---|---|---|
filter | Filter by content type or property | contentType:roomDetailPage |
sort | Sort results | sortOrder:asc, updateDate:desc |
skip | Pagination offset | 0, 12, 24 |
take | Page size | 12 |
fetch | Fetch children of a path | children:/accommodations |
expand | Expand related content | properties[relatedRooms] |
Example — List all rooms for a site:
GET /umbraco/delivery/api/v2/content?filter=contentType:roomDetailPage&sort=sortOrder:asc&take=12&skip=0Accept-Language: ptStart-Item: savoy-palace5.3 Fetch by GUID
Section titled “5.3 Fetch by GUID”GET /content/{id}Used when content references are stored as GUIDs (e.g., Content Picker values).
5.4 Media Endpoints
Section titled “5.4 Media Endpoints”GET /media/{id} # Single media item by GUIDGET /media # Query media itemsMedia responses include url, name, width, height, mediaType, bytes.
6. Frontend API Client
Section titled “6. Frontend API Client”The client lives in packages/cms-client/src/client.ts and provides typed methods:
class UmbracoClient { // Fetch a single page by its URL path async getContentByPath(siteKey: string, locale: string, path: string): Promise<PageContent | null>
// List content items by type (rooms, news, offers, etc.) async getContentByType( siteKey: string, locale: string, contentType: string, options?: { skip?: number; take?: number; sort?: string } ): Promise<PaginatedResult<UmbracoContent>>
// Fetch navigation tree for a site async getNavigation(siteKey: string, locale: string): Promise<NavItem[]>
// Fetch site root configuration async getSiteRoot(siteKey: string, locale: string): Promise<SiteRootContent>}Key implementation details:
- All fetches use
cache: 'no-store'— Cloudflare handles caching, not Next.js - 404 responses return
null(not an error) - Other error codes throw
ApiError(status, body)
7. TypeScript Types
Section titled “7. TypeScript Types”Defined in packages/cms-client/src/types.ts:
interface UmbracoContent { name: string; route: { path: string; startItem: { id: string; path: string } }; contentType: string; cultures: Record<string, { path: string; startItem: { path: string } }>; properties: Record<string, any>;}
interface UmbracoElement { contentType: string; properties: Record<string, any>;}
interface UmbracoBlockList { items: UmbracoElement[];}
interface UmbracoMedia { url: string; name: string; width?: number; height?: number; mediaType: string; bytes?: number;}
interface PageContent { name: string; contentType: string; locale: string; path: string; siteKey: string; seo: { metaTitle?: string; metaDescription?: string; noIndex?: boolean; noFollow?: boolean; canonicalUrl?: string; ogTitle?: string; ogDescription?: string; ogImage?: UmbracoMedia; }; modules: UmbracoElement[]; availableLocales: string[];}8. Pagination Pattern
Section titled “8. Pagination Pattern”Standard pagination across all list pages:
const PAGE_SIZE = 12;
async function getRoomsList(siteKey: string, locale: string, page: number) { const skip = (page - 1) * PAGE_SIZE; return client.getContentByType(siteKey, locale, 'roomDetailPage', { skip, take: PAGE_SIZE, sort: 'sortOrder:asc', });}9. Multi-Language API Behavior
Section titled “9. Multi-Language API Behavior”| Scenario | API Behavior |
|---|---|
| Page published in requested locale | Returns content in that locale |
| Page NOT published in requested locale | Returns 404 |
| Media with locale variant | Returns locale-specific media if available |
| Media without locale variant | Returns PT (default) media |
| Invariant properties (toggles, numbers) | Same value regardless of Accept-Language |
The cultures object in responses indicates which locales are available and their URL paths. Use this for:
- Language switcher (link to alternate locale)
- Hreflang meta tags
10. Umbraco Configuration
Section titled “10. Umbraco Configuration”Enable the Content Delivery API in appsettings.json:
{ "Umbraco": { "CMS": { "DeliveryApi": { "Enabled": true, "PublicAccess": true, "MemberAuthorization": { "Enabled": false } }, "WebRouting": { "DisableAlternativeTemplates": true, "DisableFindContentByIdPath": true }, "Global": { "ReservedUrls": "~/.well-known" } } }}11. Testing API Responses
Section titled “11. Testing API Responses”Before any FE integration, validate API responses match the expected shape:
Checklist:
- Content type alias matches FE registry key
- All property aliases match
.types.tsinterface fields - Block List
itemsarray is present and correctly structured - Media URLs are absolute and accessible
-
culturesobject includes all published locales -
Start-Itemcorrectly scopes to single site - Nested Element Types (e.g.,
heroSlideinsideheroSlider) are properly serialized
Quick test with curl:
curl -s \ -H "Accept-Language: pt" \ -H "Start-Item: savoy-palace" \ "${UMBRACO_API_URL}/umbraco/delivery/api/v2/content/item/" \ | jq '.properties.modules.items[0]'12. Common Pitfalls
Section titled “12. Common Pitfalls”| Pitfall | Solution |
|---|---|
Missing Start-Item header | Always include — without it, queries return cross-site results |
| Caching API responses in Next.js | Use cache: 'no-store' — Cloudflare is the cache layer |
| Assuming locale fallback | There is NO cross-language fallback — missing locale = 404 |
| Hardcoding API base URL | Always use UMBRACO_API_URL env var |
| Not expanding related content | Use expand parameter for Content Picker references |
| Property alias mismatch with FE | Coordinate aliases between BE model and packages/cms-client/src/types.ts |