06 — Custom API Endpoints
Dev Guide — Savoy Signature Hotels
PRD refs:08_API_Contracts.md,12_Forms_and_Data_Collection.md,10_MultiLanguage_and_i18n.md
1. Overview
Section titled “1. Overview”Beyond the built-in Content Delivery API, the platform requires custom .NET controllers in Umbraco for search, forms, redirects, sitemap, dictionary, and cache management. This guide documents each endpoint, its contract, and implementation requirements.
2. Custom Endpoints Summary
Section titled “2. Custom Endpoints Summary”| Endpoint | Method | Purpose |
|---|---|---|
/api/search | GET | Full-text search via Examine/Lucene |
/api/forms/{id}/submit | POST | Submit form data |
/api/redirects/{path} | GET | Check redirect configuration |
/api/sitemap/{siteKey} | GET | Generate sitemap XML data |
/api/dictionary | GET | Fetch UI label translations |
/api/cache/purge | POST | Manual cache purge from dashboard |
/api/webhooks/umbraco | POST | Publish webhook handler (see Guide 04) |
3. Search API
Section titled “3. Search API”3.1 Request
Section titled “3.1 Request”GET /api/search?q={query}&site={siteKey}&locale={locale}&page={page}&pageSize={pageSize}| Parameter | Type | Required | Default |
|---|---|---|---|
q | string | Yes | — |
site | SiteKey | Yes | — |
locale | pt | en | Yes | — |
page | number | No | 1 |
pageSize | number | No | 10 |
3.2 Response
Section titled “3.2 Response”{ "query": "ocean view", "total": 15, "page": 1, "pageSize": 10, "totalPages": 2, "items": [ { "id": "guid-here", "name": "Deluxe Ocean Room", "contentType": "roomDetailPage", "path": "/en/accommodation/deluxe-ocean-room", "excerpt": "...with stunning <mark>ocean view</mark> from every...", "score": 0.95, "imageDesktop": { "url": "...", "width": 600, "height": 400 }, "imageMobile": { "url": "...", "width": 375, "height": 250 } } ]}3.3 Implementation Notes
Section titled “3.3 Implementation Notes”- Uses Examine/Lucene (Umbraco’s built-in search engine)
- Index must be scoped per site (filter by
siteRootancestor) - Results filtered by locale (only return content published in requested language)
- Excerpt generation: highlight search terms with
<mark>tags - Rate limit: 20 req/min per IP (Cloudflare WAF rule)
3.4 Search Index Configuration
Section titled “3.4 Search Index Configuration”Create a custom Examine index scoped to published content:
- Include: All page Document Types (exclude Element Types)
- Fields:
name,metaTitle,metaDescription, full-text content fields - Exclude: Draft content, unpublished nodes, archived items
4. Forms API
Section titled “4. Forms API”4.1 Form Types
Section titled “4.1 Form Types”| Form | Placement | Action |
|---|---|---|
| Contact Us | contactPage | Email to hotel department |
| Newsletter Signup | Footer (global) | Subscribe to CRM list |
| Event Enquiry | eventsPage | RFP to Sales team |
| Wedding Enquiry | celebrationsPage | RFP to Events team |
| Spontaneous Application | Careers page | Upload CV |
4.2 Submit Request
Section titled “4.2 Submit Request”POST /api/forms/{id}/submitContent-Type: application/json{ "formId": "guid-here", "siteKey": "savoy-palace", "locale": "pt", "fields": { "name": "John Doe", "department": "reservations", "message": "I'd like to enquire about..." }, "honeypot": "", "recaptchaToken": "token-from-recaptcha-v3"}4.3 Submit Response
Section titled “4.3 Submit Response”Success:
{ "success": true, "message": "Thank you for your enquiry. We will respond within 48 hours."}Validation error:
{ "success": false, "message": "Please correct the errors below.", "errors": { "email": ["Invalid email address"], "name": ["This field is required"] }}4.4 Security — 5 Layers
Section titled “4.4 Security — 5 Layers”| Layer | Implementation |
|---|---|
| 1. Honeypot | Hidden field named website — reject if filled |
| 2. Rate Limiting | Max 5 submissions per IP per 5 minutes (Next.js API + Cloudflare WAF) |
| 3. reCAPTCHA v3 | Invisible token — block if score < 0.5 |
| 4. Server Validation | Verify payload matches form field definitions in Umbraco |
| 5. CSRF Protection | Next.js + CORS — verify request origin is a Savoy domain |
4.5 Email Dispatch
Section titled “4.5 Email Dispatch”| Setting | Value |
|---|---|
| Provider | Mailjet or SendGrid (configurable via Azure App Settings) |
| Sender | [email protected] |
| DNS records | SPF, DKIM, DMARC configured in Cloudflare DNS |
| Reply-To | Submitted email address |
| Routing | Based on “Department” field or siteRoot config |
4.6 File Uploads (Careers Form)
Section titled “4.6 File Uploads (Careers Form)”- Max size: 5MB
- Allowed types:
.pdf,.docxonly - Storage: Azure Blob
/form-uploadscontainer (private) - Email: Contains secure download link (expiring SAS token), NEVER the file itself
- Retention: Auto-delete after 90 days (GDPR)
4.7 GDPR Compliance
Section titled “4.7 GDPR Compliance”- Mandatory consent checkbox linking to Privacy Policy page
- Umbraco Forms data auto-deleted after 90 days via scheduled task
- Right to Erasure: DPO can delete individual entries via backoffice
- Export: Data exportable to Excel/CSV for data subject requests
5. Redirects API
Section titled “5. Redirects API”5.1 Request
Section titled “5.1 Request”GET /api/redirects/{path}5.2 Response
Section titled “5.2 Response”Redirect found:
{ "found": true, "statusCode": 301, "destination": "/en/accommodation"}No redirect:
{ "found": false}5.3 Implementation Notes
Section titled “5.3 Implementation Notes”- Check Umbraco’s built-in URL Tracker for content moves/renames
- Support custom redirect rules configured by editors in backoffice
proxy.tschecks this endpoint before rendering a 404
6. Sitemap API
Section titled “6. Sitemap API”6.1 Request
Section titled “6.1 Request”GET /api/sitemap/{siteKey}?locale={locale}6.2 Response
Section titled “6.2 Response”Returns sitemap data as JSON (Next.js generates the XML):
{ "urls": [ { "loc": "/pt/", "lastmod": "2026-03-05", "changefreq": "weekly", "priority": 1.0, "alternates": [ { "hreflang": "pt", "href": "https://www.savoysignature.com/pt/" }, { "hreflang": "en", "href": "https://www.savoysignature.com/en/" }, { "hreflang": "x-default", "href": "https://www.savoysignature.com/pt/" } ] } ]}6.3 Implementation Notes
Section titled “6.3 Implementation Notes”- Include all published pages for the requested site
- Exclude pages with
noIndex: true - Include
<xhtml:link rel="alternate" hreflang="...">annotations for multi-language x-defaultalways points to the PT versionlastmodis the UmbracoupdateDate
7. Dictionary API
Section titled “7. Dictionary API”7.1 Request
Section titled “7.1 Request”GET /api/dictionary?locale={locale}7.2 Response
Section titled “7.2 Response”{ "ReadMore": "Read More", "BookNow": "Book Now", "ViewAll": "View All", "BackToTop": "Back to Top", "Close": "Close", "Loading": "Loading...", "Menu": "Menu", "Search": "Search", "Language": "Language", "SkipToContent": "Skip to Content", "CheckIn": "Check-in", "CheckOut": "Check-out", "Guests": "Guests", "Adults": "Adults", "Children": "Children", "PromoCode": "Promo Code", "SearchAvailability": "Search Availability", "Submit": "Submit", "RequiredField": "Required field", "InvalidEmail": "Invalid email", "SuccessMessage": "Thank you for your submission", "From": "From", "PerNight": "/night", "SqMeters": "sqm", "MaxGuests": "Max Guests", "Amenities": "Amenities", "PageNotFound": "Page Not Found", "ServerError": "Server Error", "TryAgain": "Try Again"}7.3 Complete Dictionary Key Registry
Section titled “7.3 Complete Dictionary Key Registry”| Category | Keys |
|---|---|
| Global | ReadMore, BookNow, ViewAll, BackToTop, Close, Loading |
| Navigation | Menu, Search, Language, SkipToContent |
| Booking | CheckIn, CheckOut, Guests, Adults, Children, PromoCode, SearchAvailability |
| Forms | Submit, RequiredField, InvalidEmail, SuccessMessage |
| Rooms | From, PerNight, SqMeters, MaxGuests, Amenities |
| Errors | PageNotFound, ServerError, TryAgain |
7.4 Implementation Notes
Section titled “7.4 Implementation Notes”- Uses Umbraco Dictionary Items
- Cached in Next.js with
use cache+cacheLife('minutes')(~15 min TTL) - Fetched with
cache: 'no-store'at the API level
8. Cache Purge API (Dashboard)
Section titled “8. Cache Purge API (Dashboard)”8.1 Request
Section titled “8.1 Request”POST /api/cache/purgeAuthorization: Bearer {ADMIN_TOKEN}{ "type": "all" | "site" | "urls", "siteKey": "savoy-palace", "urls": ["/pt/alojamento", "/en/accommodation"]}8.2 Implementation Notes
Section titled “8.2 Implementation Notes”- Only accessible from Umbraco backoffice (authenticated admin)
- Calls Cloudflare purge API
- Logs all purge operations for audit trail
- Three purge types: all (purge everything), site (all URLs for a site), urls (specific URLs)
9. Form Schema Endpoint (for Frontend Dynamic Rendering)
Section titled “9. Form Schema Endpoint (for Frontend Dynamic Rendering)”Editors build forms in Umbraco Forms (drag-and-drop). The frontend needs the form definition to render dynamically.
9.1 Request
Section titled “9.1 Request”GET /api/forms/{id}9.2 Response
Section titled “9.2 Response”{ "formId": "guid-here", "name": "Contact Us", "submitLabel": "Send Message", "successMessage": "Thank you for contacting us.", "fields": [ { "alias": "name", "label": "Full Name", "type": "text", "required": true, "cssClass": "col-span-12 md:col-span-6", "validation": { "maxLength": 100 } }, { "alias": "email", "label": "Email Address", "type": "email", "required": true, "cssClass": "col-span-12 md:col-span-6", "validation": { "pattern": "^[^@]+@[^@]+\\.[^@]+$" } }, { "alias": "department", "label": "Department", "type": "dropdown", "required": true, "cssClass": "col-span-12", "options": [ { "value": "reservations", "label": "Reservations" }, { "value": "events", "label": "Events" }, { "value": "general", "label": "General Enquiry" } ] }, { "alias": "message", "label": "Your Message", "type": "textarea", "required": true, "cssClass": "col-span-12", "validation": { "maxLength": 2000 } }, { "alias": "consent", "label": "I agree to the Privacy Policy", "type": "checkbox", "required": true, "cssClass": "col-span-12" } ]}9.3 Frontend Rendering
Section titled “9.3 Frontend Rendering”The frontend uses react-hook-form + zod to:
- Fetch form schema JSON
- Dynamically generate a Zod validation schema from field rules
- Render form fields based on
typeandcssClass - Submit via POST to
/api/forms/{id}/submit
10. Rate Limiting
Section titled “10. Rate Limiting”| Endpoint | Limit | Enforced By |
|---|---|---|
/api/search | 20 req/min per IP | Cloudflare WAF |
/api/forms/*/submit | 5 req/5min per IP | Cloudflare WAF + Next.js API |
All other /api/* | 60 req/min per IP | Cloudflare WAF |
11. Common Pitfalls
Section titled “11. Common Pitfalls”| Pitfall | Solution |
|---|---|
| Search index includes draft content | Configure Examine index to only include published nodes |
| Form submissions without reCAPTCHA | Always validate reCAPTCHA token server-side (score >= 0.5) |
| File attached directly in email | Never attach files — use secure download links |
| Dictionary not cached | Use use cache with ~15 min TTL in Next.js |
| Sitemap includes noIndex pages | Filter out pages where noIndex === true |
| Missing CORS headers on custom endpoints | Configure CORS for Next.js origin only |
| Search results cross-site | Always filter search index by siteRoot ancestor |