Content Blocks
Every product, experience, and store product returned by the API includes a content_blocks object with the supplier-authored sections used by the booking widget's detail pages, booking confirmation emails, and manifests. The same shape is emitted for all three resources โ a client that renders one can render all three.
Products and experiences carry one extra key, custom_sections โ an ordered list of free-form { heading, body } blocks the supplier adds beyond the fixed sections. Store products have no custom sections, so the key is always null there.
The four prose fields (check_in_details, restrictions, health_and_safety, cancellation_notes) and every custom-section body are allowlisted rich-text HTML โ see Rich text below.
Shapeโ
{
"content_blocks": {
"whats_included": [
{ "label": "All equipment", "description": "Board, leash, wetsuit" },
{ "label": "Instructor", "description": null }
],
"does_not_include": [
{ "label": "Meals and drinks", "description": "Lunch is not provided" },
{ "label": "Gratuities", "description": null }
],
"itinerary": [
{ "time": "9:00 AM", "title": "Check-in", "description": "Meet at the beach hut" },
{ "time": "9:30 AM", "title": "Lesson", "description": "Introduction to paddling" },
{ "time": "11:00 AM", "title": "Open surf", "description": null }
],
"highlights": [
"Small groups (4 max)",
"Photos included"
],
"check_in_details": "<p>Meet at the <strong>red umbrella</strong> next to the kiosk, 15 min before start time.</p>",
"what_to_bring": [
"Swimwear",
"Sunscreen",
"Towel"
],
"restrictions": "<p>Minimum age 8. Must be comfortable in the water.</p>",
"health_and_safety": "<p>Lifeguard on duty. Wetsuits provided year-round.</p>",
"cancellation_notes": "<p>Free cancellation up to <strong>24 hours</strong> before the booking.</p>",
"custom_sections": [
{ "heading": "Parking", "body": "<p>Free lot behind the surf school. <a href=\"https://maps.example.com/lot\">Directions</a>.</p>" },
{ "heading": "Weather policy", "body": "<p>Lessons run rain or shine. We only cancel for <strong>lightning</strong> or unsafe surf.</p>" }
],
"location": {
"mode": "location",
"location_id": 12,
"address": "184 Phillip St, Sydney, NSW, 2000, Australia"
}
}
}
Fieldsโ
| Field | Type | Description |
|---|---|---|
whats_included | array|null | Ordered list of { label, description } rows. description may be null. |
does_not_include | array|null | Ordered list of { label, description } rows โ things the customer must arrange or pay for themselves. description may be null. |
itinerary | array|null | Ordered list of { time, title, description } rows. time and description may be null; title is always present on non-empty rows. Use for multi-step activities. |
highlights | array|null | Flat list of plain strings, rendered as pill chips on the widget. |
check_in_details | string|null | Rich-text HTML. |
what_to_bring | array|null | Flat list of plain strings, rendered as pill chips. |
restrictions | string|null | Rich-text HTML (age / weight / skill requirements). |
health_and_safety | string|null | Rich-text HTML. |
cancellation_notes | string|null | Customer-facing cancellation notes. Rich-text HTML. Independent of the cancellation policy tiers attached to the booking. |
location | object|null | Products & experiences only. Where the item happens, shown as a map under the check-in / pickup section on the booking page. null when the supplier has not set one. See Location. |
custom_sections | array|null | Products & experiences only. Ordered list of { heading, body } rows beyond the eight fixed sections. heading is plain text; body is rich-text HTML. Always null on store products. |
Any field with no content from the supplier comes back as null. Empty arrays and empty prose are both normalised to null โ clients should check == null (or truthy) rather than length to decide whether to render a section.
Locationโ
| Field | Type | Description |
|---|---|---|
mode | string | "location" uses one of the supplier's saved locations. "custom" uses an address typed for this item. "pickup" (products only) follows whichever pickup branch the customer chooses on the booking page. |
location_id | integer|null | The saved location, set only when mode is "location". |
address | string|null | The resolved one-line address, whatever the mode. This is the value the booking page shows and the value Google is queried with, so a client never has to reassemble it. |
location is null when the supplier has set no map, and also when the answer no longer resolves - a saved location deleted since, or a blank typed address. In those cases nothing is shown rather than a wrong pin.
address on a "pickup" item reflects the supplier's default pickup branch. It changes per booking on the booking page, so treat it as a default rather than a fixed value.
custom_sections rowโ
| Field | Type | Description |
|---|---|---|
heading | string | Section title, plain text. Always present on a non-empty row. |
body | string | Section content as rich-text HTML. |
Rows are returned in the order the supplier arranged them. A section where both heading and body are blank is dropped on save, so every returned row has content.
Rich textโ
The four prose fields (check_in_details, restrictions, health_and_safety, cancellation_notes) and every custom-section body hold allowlisted rich-text HTML, authored with a small formatting toolbar in the portal. On read they come back as sanitized HTML; render them as markup (not plain text).
Only this tag allowlist survives sanitization โ everything else is unwrapped to its text content:
<p> ยท <br> ยท <strong> ยท <em> ยท <u> ยท <a> ยท <ul> ยท <ol> ยท <li> ยท <h3>
The only attribute kept is a safe href on <a> (schemes http, https, mailto, tel); style, class, on* handlers, and javascript: / data: URLs are stripped. A block that sanitizes down to no visible text (e.g. an empty <p><br></p>) is normalised to null.
These four fields previously returned plain text (HTML stripped on save). They now return allowlisted HTML. Clients that injected the old plain-text values into the DOM should switch to rendering the value as HTML โ or strip tags client-side if a plain-text presentation is required. The arrays and repeater rows (whats_included, itinerary, highlights, what_to_bring) and custom-section headings remain plain text.
Write-side (portal vs API)โ
Whatever surface writes content blocks โ the Livewire portal or the MCP tools create_product / update_product / create_experience / create_store_product / update_store_product โ the payload runs through one sanitizer before it's stored, so the shape on disk is identical regardless of the channel that wrote it. Sanitization:
- Drops repeater rows (
whats_included,does_not_include,itinerary) and list items (highlights,what_to_bring) where every field is blank; an array that empties out becomesnull. - Sanitizes the four prose fields and every custom-section
bodyto the rich-text allowlist; a block with no visible text becomesnull. - Trims and plain-text-strips each custom-section
heading, and drops any section where bothheadingandbodyare empty.
custom_sections is accepted in the same payload as the other content blocks (products and experiences only). Omitting a key leaves the stored value untouched โ supplying it (including an empty array, which normalises to null) overwrites it.
The REST API doesn't currently accept content-block writes โ use the MCP tools, or the portal, to edit them.