Service catalogs and locked booking
Build service catalogs, calendar-first browsing, service detail pages, and booking embeds for a selected service or category.
Build service catalogs, calendar-first browsing, service detail pages, and booking embeds for a selected service or category. First, complete the installation.
Render a customer flow service catalog
Use service attributes to build a custom catalog from the services connected to a customer flow. Your Webflow or website-builder elements remain in full control of the layout and styling.
Service catalogs and locked booking require Hosted Package 1.1.22 or
newer. Automatic category-image fallback and the explicit service/category
image fields require 1.1.23 or newer. Service category-name fields require
1.1.24 or newer. Service category-description fields require 1.1.25 or
newer. The unversioned installation URL loads the current version.
Set data-tiquo-services to the customer flow ID. The first
data-tiquo-service-item child is the template that Tiquo hides and clones for
each service.
<div data-tiquo-services="CUSTOMER_FLOW_ID" data-tiquo-limit="20">
<article data-tiquo-service-item>
<a
data-tiquo-service-link="/services/{slug}?tiquoServiceId={id}"
>
<img data-tiquo-service="image" alt="" />
<h2 data-tiquo-service="name"></h2>
<span data-tiquo-service="categoryName"></span>
<p data-tiquo-service="categoryDescription"></p>
</a>
<p data-tiquo-service="shortDescription"></p>
<span data-tiquo-service="priceLabel"></span>
<span data-tiquo-service="durationLabel"></span>
</article>
<p data-tiquo-empty>No services available.</p>
</div>Customer flow service catalog
Inspect the catalog wrapper, template, link, every supported service field, images, pricing, booking rules, and fallback.
Experiences
Book your next visit
Services available at Atlas House.
Recovery Suite
Private sauna, cold plunge, and a quiet recovery lounge.
Every other supported service field
39 fieldsService ID
service_01JZ8A2K
Service ID alias
service_01JZ8A2K
Service slug
recovery-suite
Full description
Private sauna, cold plunge, and lounge.
Short description
Private recovery experience.
Service tags
Wellness, Recovery
Tags label
Wellness, Recovery
Featured image
Featured service image
Service image
First service image
Service image URLs
2 image URLs
Category image
Wellness category image
Category image URLs
2 category image URLs
First category name
Wellness
All category names
Wellness, Members
Category names label
Wellness, Members
All category descriptions
Restore and recharge, Member favourites
Base price
35.00
Effective price
28.00
Currency
GBP
Price type
fixed
Service type
appointment
Raw duration
60
Duration type
minutes
Capacity
4
Featured service
Yes
Payment type
online
Deposit required
Yes
Deposit amount
10.00
Deposit type
fixed
Minimum advance booking
2 hours
Maximum advance booking
90 days
Check-in time
15:00
Check-out time
11:00
Minimum stay
1 night
Maximum stay
14 nights
Category IDs
cat_wellness, cat_members
Category IDs label
cat_wellness, cat_members
Customer flow ID
flow_atlas_house
Locked booking URL
/embed/flow_atlas_house?service=...
The first service card is the template. Tiquo safely fills links, images, and text according to the element each data attribute is attached to.
The customer flow ID is the ID in its booking or embed URL. For example, use
CUSTOMER_FLOW_ID from
https://book.tiquo.app/embed/CUSTOMER_FLOW_ID.
data-tiquo-service-link sets the element's href and supports these
URL-encoded placeholders:
| Placeholder | Value |
|---|---|
{id} | Service ID |
{slug} | Service slug |
{bookingConfigId} | Customer flow ID |
If the attribute has no value, its default link is
?tiquoServiceId={id}. Use data-tiquo-limit to cap the number of services;
the default is 100.
Public services render without a customer session. When the customer is signed in, the collection also includes membership-gated services that customer is allowed to book. Inactive services, services outside their visibility window, and services unavailable to the current customer are not rendered.
Service fields
Set data-tiquo-service to any supported field:
| Field | Description |
|---|---|
id or serviceId | Service ID |
slug | Service slug |
name | Service name |
description | Full description |
shortDescription | Short description |
tags | Comma-separated service tags |
tagsLabel | Comma-separated service tags |
image | Preferred display image: the service image, then the first associated category image |
featuredImage | Featured service image, with the first service image as fallback |
serviceImage | Featured service image, with the first service image as fallback |
images | Comma-separated service gallery URLs (up to 15). On an img, the first image |
categoryImage | Primary image of the first flow category containing the service |
categoryImages | Comma-separated primary images of the flow categories containing the service |
categoryName | Name of the first flow category containing the service |
categoryNames | Comma-separated names of all flow categories containing the service |
categoryNamesLabel | Comma-separated names of all flow categories containing the service |
categoryDescription | First configured description from a flow category containing the service |
categoryDescriptions | Comma-separated configured descriptions from all flow categories containing the service |
price | Base price |
effectivePrice | Effective price after applicable flow pricing |
currency | Currency code |
priceLabel | Localized effective price, or Free |
priceType | Price type |
serviceType | Service type |
duration | Raw duration |
durationType | Duration unit or booking type |
durationLabel | Display duration, such as 60 minutes |
capacity | Most guests one booking can hold: the largest room or table that seats one party. Empty for classes and events that sell spots from a shared pool |
location.name, location.sublocationName | Venue and sublocation where the service is booked |
location.address, location.city, location.state, location.postalCode, location.country | Venue address. A sublocation's own address replaces the venue address when one is set |
location.timezone | Venue timezone |
isFeatured | Yes or No |
paymentType | Payment type |
depositRequired | Yes or No |
depositAmount | Deposit amount |
depositType | Deposit type |
minAdvanceBooking | Minimum advance-booking rule |
maxAdvanceBooking | Maximum advance-booking rule |
checkInTime | Check-in time |
checkOutTime | Check-out time |
minStay | Minimum stay |
maxStay | Maximum stay |
categoryIds | Comma-separated category IDs |
categoryIdsLabel | Comma-separated category IDs |
bookingConfigId | Customer flow ID |
bookingUrl | Locked booking URL for this service |
When data-tiquo-service="image" or another image URL field is placed on an
img, Tiquo safely sets src. On a link, it safely sets href. The existing
data-tiquo-hide-empty, data-tiquo-preserve-text, and data-tiquo-style
data attributes also work with service fields.
image is the simplest choice for catalog cards because it prefers an image
configured directly on the service and automatically falls back to the first
associated category image. Use serviceImage or categoryImage when the
image source must be explicit. The plural images and categoryImages fields
render as comma-separated URLs on text elements.
For example, use a service image as a background:
<div
data-tiquo-service="image"
data-tiquo-style="background-image"
data-tiquo-hide-empty
></div>Image galleries
Services and categories each hold an ordered gallery of up to 15 images. To
build your own carousel, add a data-tiquo-service-images container with one
data-tiquo-service-image template inside a service item or detail page.
Tiquo copies the template once per image:
<div data-tiquo-service-images data-tiquo-hide-empty>
<img data-tiquo-service-image alt="" />
</div>| Attribute | Description |
|---|---|
data-tiquo-service-images | The gallery container. Leave the value empty for the service's images, falling back to its category's gallery when the service has none. Use service for service images only, or category for the gallery of the first flow category containing the service |
data-tiquo-service-image | The template copied for each image. On an img, Tiquo sets src; with data-tiquo-style="background-image", it sets the background; otherwise it sets the src of each img inside |
data-tiquo-limit | Optional: the most images to show, up to the default of 15 |
data-tiquo-hide-empty | Optional: hides the container when there are no images |
Tiquo sets data-tiquo-image-count on the container to the number of images
and data-tiquo-image-index on each copy, which you can use for carousel
controls and styling.
Use categoryName to display the first associated category. If a service is
assigned to multiple flow categories, use categoryNames to display every
category as comma-separated text:
<span data-tiquo-service="categoryName"></span>
<span data-tiquo-service="categoryNames"></span>Use categoryDescription to display the first configured description from an
associated category. Use categoryDescriptions to display every configured
associated category description as comma-separated text:
<p data-tiquo-service="categoryDescription"></p>
<p data-tiquo-service="categoryDescriptions"></p>To render the two sources separately:
<img
data-tiquo-service="serviceImage"
data-tiquo-hide-empty
alt=""
/>
<img
data-tiquo-service="categoryImage"
data-tiquo-hide-empty
alt=""
/>Render a custom service detail page
Set data-tiquo-service-detail to the same customer flow ID:
<section data-tiquo-service-detail="CUSTOMER_FLOW_ID">
<img data-tiquo-service="image" alt="" />
<h1 data-tiquo-service="name"></h1>
<p data-tiquo-service="description"></p>
<p data-tiquo-service="categoryDescription"></p>
<span data-tiquo-service="priceLabel"></span>
<div data-tiquo-service-booking></div>
<p data-tiquo-empty>
This service is unavailable.
</p>
</section>Service detail and booking
Inspect the detail wrapper, every available service field, the locked booking mount, and unavailable-service fallback.
Wellness
Recovery Suite
Reset with a private sauna, cold plunge, and a calm recovery lounge.
Every other service field available on the detail page
40 fieldsService ID
service_01JZ8A2K
Service ID alias
service_01JZ8A2K
Service slug
recovery-suite
Short description
Private recovery experience.
Service tags
Wellness, Recovery
Tags label
Wellness, Recovery
Featured image
Featured service image
Service image
First service image
Service image URLs
2 image URLs
Category image
Wellness category image
Category image URLs
2 category image URLs
First category name
Wellness
All category names
Wellness, Members
Category names label
Wellness, Members
First category description
Restore, reset, and recharge.
All category descriptions
Restore and recharge, Member favourites
Base price
35.00
Effective price
28.00
Currency
GBP
Price type
fixed
Service type
appointment
Raw duration
60
Duration type
minutes
Duration label
60 minutes
Capacity
4
Featured service
Yes
Payment type
online
Deposit required
Yes
Deposit amount
10.00
Deposit type
fixed
Minimum advance booking
2 hours
Maximum advance booking
90 days
Check-in time
15:00
Check-out time
11:00
Minimum stay
1 night
Maximum stay
14 nights
Category IDs
cat_wellness, cat_members
Category IDs label
cat_wellness, cat_members
Customer flow ID
flow_atlas_house
Locked booking URL
/embed/flow_atlas_house?service=...
Locked booking flow
Choose a time
The booking mount becomes an auto-resizing iframe locked to this service. A fixed service ID is optional when the URL already contains tiquoServiceId.
By default, the detail container reads the service ID from the
tiquoServiceId query parameter written by the catalog link. To render a fixed
service instead, set it directly:
<section
data-tiquo-service-detail="CUSTOMER_FLOW_ID"
data-tiquo-service-id="SERVICE_ID"
>
<!-- Service fields and booking container -->
</section>data-tiquo-service-booking becomes an automatically resizing customer-flow
iframe. The requested service is preselected, category and service-catalog
navigation are removed, and the customer cannot return to select another
service. The remaining availability, quantity, payment, and booking steps
continue normally.
If the service is missing or unavailable to the current visitor, Tiquo clears
the service fields, hides the booking container, and shows
data-tiquo-empty. It never falls back to the full flow catalog.
Calendar-first service browsing
Use Tiquo.getFlowAvailability(bookingConfigId, options) to display dates
first and show available services after a visitor clicks a date. This method
is available in Hosted Package 1.2.2 and newer. The unversioned installation
URL loads the current version.
Build this interaction with the JavaScript API after window.Tiquo.ready.
The data-tiquo-services collection renders a service catalog; it does not
create calendar buttons or filter that collection when a date is selected.
The example below renders the calendar and service list explicitly.
<div id="availability-calendar" aria-label="Available dates"></div>
<p id="availability-status" role="status"></p>
<ul id="available-services"></ul>
<div id="calendar-booking"></div>
<script>
window.Tiquo.ready(async function (tiquo) {
const flowId = 'CUSTOMER_FLOW_ID';
const calendar = document.querySelector('#availability-calendar');
const serviceList = document.querySelector('#available-services');
const status = document.querySelector('#availability-status');
try {
const summary = await tiquo.getFlowAvailability(flowId, {
dateFrom: '2026-09-14',
dateTo: '2026-09-20',
partySize: 2,
});
if (summary.partial) {
throw new Error('Some availability could not be loaded. Please retry.');
}
const servicesById = new Map(
summary.services.map((service) => [service.id, service]),
);
calendar.replaceChildren();
serviceList.replaceChildren();
status.textContent = summary.days.some((day) => day.isAvailable)
? 'Choose a date to see available services.'
: 'No services are available this week.';
for (const day of summary.days) {
const dateButton = document.createElement('button');
dateButton.type = 'button';
dateButton.textContent = day.date;
dateButton.disabled = !day.isAvailable;
dateButton.setAttribute('aria-pressed', 'false');
dateButton.addEventListener('click', () => {
for (const button of calendar.querySelectorAll('button')) {
button.setAttribute('aria-pressed', String(button === dateButton));
}
serviceList.replaceChildren();
status.textContent = 'Available services for ' + day.date;
for (const serviceId of day.serviceIds) {
const service = servicesById.get(serviceId);
if (!service) continue;
const item = document.createElement('li');
const bookButton = document.createElement('button');
bookButton.type = 'button';
bookButton.textContent = service.name;
bookButton.addEventListener('click', async () => {
bookButton.disabled = true;
try {
await tiquo.embedServiceBooking(
flowId, service.id, '#calendar-booking',
{ date: day.date, partySize: 2 },
);
} catch (error) {
status.textContent = 'Booking could not be loaded. Please retry.';
} finally {
bookButton.disabled = false;
}
});
item.append(bookButton);
serviceList.append(item);
}
});
calendar.append(dateButton);
}
} catch (error) {
status.textContent = error.message || 'Availability could not be loaded.';
}
});
</script>Replace the example date range with the week the visitor is browsing.
Availability options and response
| Option | Description |
|---|---|
dateFrom | First venue-local date, as YYYY-MM-DD |
dateTo | Last venue-local date, inclusive; at most seven dates per request |
partySize | People in one booking; integer from 1 to 1000, default 1. Rooms and tables are offered only when one fits the whole party; classes and events need this many spots free |
includeSlots | Include exact time slots; default false. When true, request only one day |
lengthMinutes | Booking length to check, in minutes (5 to 1440), for services where the customer chooses the length. Services that don't offer that length are left out; fixed-length services ignore it |
Fetch additional calendar pages by advancing the date range one week at a time. Use dates in the venue's timezone, rather than converting them from the visitor's local timezone.
The response includes the same membership-filtered services, categories,
catalogSections, and flow information as getFlowServices(), plus:
| Field | Description |
|---|---|
dateFrom, dateTo, partySize | Requested range and party size |
slotsIncluded | Whether exact time slots were requested |
days | Every requested date, including unavailable dates |
days[].date | Venue-local date as YYYY-MM-DD |
days[].isAvailable | Whether any service has bookable capacity that day |
days[].serviceIds | IDs of services bookable somewhere on that date |
days[].slots | Optional time entries with time and serviceIds for timed services |
partial | Whether any service availability lookup failed |
failedServiceIds | Service IDs whose availability could not be loaded |
Match each day's serviceIds to the objects in services to render names,
images, and prices. Complete results with no availability retain the date with
an empty serviceIds array. Daily and nightly services can be available on a
date without exposing time slots.
If partial is true, show an incomplete-availability message and retry the
request. Do not treat failedServiceIds as sold out. Re-fetch availability
when the party size or customer session changes.
Load time slots after choosing a date
Run this inside the Tiquo.ready callback or a date-click handler that has
access to its tiquo argument.
// selectedDate is the YYYY-MM-DD value from the clicked calendar day.
const detail = await tiquo.getFlowAvailability('CUSTOMER_FLOW_ID', {
dateFrom: selectedDate,
dateTo: selectedDate,
partySize: 2,
includeSlots: true,
});
if (detail.partial) {
throw new Error('Some availability could not be loaded. Please retry.');
}
const servicesById = new Map(
detail.services.map((service) => [service.id, service]),
);
const slots = (detail.days[0]?.slots ?? []).map((slot) => ({
time: slot.time,
services: slot.serviceIds
.map((id) => servicesById.get(id))
.filter(Boolean),
}));
// Render slots, then show the services for the customer's chosen time.After the visitor chooses a service and time, pass the same date, start
time and party size to embedServiceBooking() so the customer doesn't choose
them again. See JavaScript API. The embedded flow re-checks
availability, and the customer can still change anything.
JavaScript API
Service catalogs and locked booking are also available programmatically:
<div id="service-booking"></div>
<script>
window.Tiquo.ready(async function (tiquo) {
const catalog = await tiquo.getFlowServices("CUSTOMER_FLOW_ID");
const service = await tiquo.getFlowService(
"CUSTOMER_FLOW_ID",
catalog.services[0].id
);
if (service) {
await tiquo.embedServiceBooking(
"CUSTOMER_FLOW_ID",
service.id,
"#service-booking",
{ minHeight: "240px" }
);
}
});
</script>getFlowService() returns null when the requested service is not available
to the current visitor. embedServiceBooking() uses the same secure
authenticated iframe handoff and automatic resizing as
embedCustomerFlow().
When your page already asked for the day, time, number of people or booking length, pass them in the options so the customer doesn't choose them again:
await tiquo.embedServiceBooking("CUSTOMER_FLOW_ID", service.id, "#service-booking", {
date: "2026-10-14", // venue-local YYYY-MM-DD
startTime: "14:00", // HH:mm on that date
partySize: 6, // the partySize you searched with
lengthMinutes: 120, // for a service where the customer chooses the length
});The calendar opens on date. startTime is selected only while it's still
free for the booking length and party size, and behaves like the customer's
own click. partySize sets the flow's people stepper, within the flow's
limits; flows that sell a quantity of tickets ignore it. lengthOptionId
pre-selects one of the service's set lengths. Everything stays editable, and
availability is checked again when the customer books. Values in the wrong
format reject the returned promise.
Embed booking for a selected category
Use embedCustomerFlow() with categoryId and lockCategory=true to embed
a category-locked booking flow. This is supported on the /embed/ surface
used by both the DOM Package and Hosted Package.
For example, a Boxing Sessions category can contain separate Member, Guest, and Public services. A category card should open that category and let the customer choose their eligible booking type in the flow's own service step. The category is preselected and cannot be changed inside the embed; the remaining steps follow the flow's configured order.
Use getFlowServices() to load the catalog and render one card per entry in
catalog.categories. Store each category's id on its card and pass the
clicked category's ID when opening the booking. Do not choose the first
service in the category or group categories by name alone.
Use the JavaScript API for this category-card pattern. The
data-tiquo-service-booking attribute and service bookingUrl field select
one service and should not be used for category cards.
<div id="category-booking"></div>
<script>
window.Tiquo.ready(async function (tiquo) {
const bookingConfigId = "CUSTOMER_FLOW_ID";
const catalog = await tiquo.getFlowServices(bookingConfigId);
const categoryId = "CATEGORY_ID"; // ID from the clicked category card.
const category = catalog.categories.find((item) => item.id === categoryId);
if (!category) throw new Error("Category is unavailable in this flow");
const url = new URL(
"https://book.tiquo.app/embed/" + encodeURIComponent(bookingConfigId)
);
url.searchParams.set("categoryId", category.id);
url.searchParams.set("lockCategory", "true");
await tiquo.embedCustomerFlow(url.toString(), "#category-booking", {
minHeight: "240px",
autoResize: true,
});
});
</script>Replace CATEGORY_ID with the clicked card's catalog.categories[].id.
CUSTOMER_FLOW_ID is the booking configuration ID already used with
getFlowServices() and found in the flow's booking URL.
Leave serviceId and lockService out of category URLs. A locked service
takes precedence and prevents the customer from choosing another booking
type. Continue using embedServiceBooking() when the customer has already
chosen one specific service.
Customer authentication and booking-type visibility
The embed uses the package's authenticated customer session and the flow's
normal membership and service-access rules. Ensure the customer is signed in
through the package before opening the embed when member options are needed.
Signing into the separate member portal alone does not establish that
website session. embedCustomerFlow() handles the authenticated iframe
handoff and automatic resizing.
Category locking narrows the selection; it does not grant access to Member, Guest, or Public services. The options offered depend on the customer's access and the flow configuration. An invalid category or one with no accessible services does not fall back to unrelated services.
Is the additional flowId parameter required?
No. A correctly configured public embed resolves the flow using the booking
configuration ID in /embed/CUSTOMER_FLOW_ID.
The portal may also include flowId, the underlying flow record ID returned
as catalog.flow.id. It is distinct from catalog.bookingConfigId and is
used for a fallback lookup where permitted. It does not authenticate the
customer or enable member booking types. Omit it from the category embed
shown above.
See Rendering and empty states for shared collection behaviour.
Booking length configuration
A returned service can include bookingLength for calendar-based Minutes or Hourly bookings. With mode: 'options', each option has an id, minutes, optional label, price, and optional isDefault. With mode: 'range', the configuration supplies minMinutes, maxMinutes, stepMinutes, defaultMinutes, and optional maxPrice.
Use this configuration to explain the available lengths in a custom catalogue. The Tiquo booking flow checks the selected length against availability and calculates the final price. A catalogue response does not reserve a slot or establish that the full duration is available.