Skip to main content

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โ€‹

FieldTypeDescription
whats_includedarray|nullOrdered list of { label, description } rows. description may be null.
does_not_includearray|nullOrdered list of { label, description } rows โ€” things the customer must arrange or pay for themselves. description may be null.
itineraryarray|nullOrdered 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.
highlightsarray|nullFlat list of plain strings, rendered as pill chips on the widget.
check_in_detailsstring|nullRich-text HTML.
what_to_bringarray|nullFlat list of plain strings, rendered as pill chips.
restrictionsstring|nullRich-text HTML (age / weight / skill requirements).
health_and_safetystring|nullRich-text HTML.
cancellation_notesstring|nullCustomer-facing cancellation notes. Rich-text HTML. Independent of the cancellation policy tiers attached to the booking.
locationobject|nullProducts & 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_sectionsarray|nullProducts & 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โ€‹

FieldTypeDescription
modestring"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_idinteger|nullThe saved location, set only when mode is "location".
addressstring|nullThe 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โ€‹

FieldTypeDescription
headingstringSection title, plain text. Always present on a non-empty row.
bodystringSection 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.

note

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 becomes null.
  • Sanitizes the four prose fields and every custom-section body to the rich-text allowlist; a block with no visible text becomes null.
  • Trims and plain-text-strips each custom-section heading, and drops any section where both heading and body are 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.