Service Catalogs and Locked Booking
Load service catalogs, build calendar-first browsing, and embed booking for a selected service or category
Use the DOM Package to load the services connected to a customer flow without rendering the complete customer-flow interface. Your application can build its own catalog and service-detail pages, then embed only the booking journey for the selected service or category.
Service catalogs and locked booking require @tiquo/dom-package 1.6.3 or
newer. Use 1.6.6 or newer for the complete typed field set documented on
this page, including explicit service/category images, names, and
descriptions.
Customer flow ID
The methods on this page use the customer flow's booking configuration ID. It is the ID in the flow's booking or embed URL.
For example, if the embed URL is:
https://book.tiquo.app/embed/CUSTOMER_FLOW_IDpass CUSTOMER_FLOW_ID as bookingConfigId.
The customer flow must belong to the same organization as the Website SDK public key used to initialize the package. The current website domain must also be enabled under Settings > Website SDK.
Load a service catalog
Call getFlowServices() to load the active services connected to a customer
flow:
const catalog = await tiquo.getFlowServices('CUSTOMER_FLOW_ID');
for (const service of catalog.services) {
console.log(service.id);
console.log(service.name);
console.log(service.image);
console.log(service.serviceImage);
console.log(service.categoryImage);
console.log(service.categoryName);
console.log(service.categoryNames);
console.log(service.categoryDescription);
console.log(service.categoryDescriptions);
console.log(service.effectivePrice);
}Public services can be loaded without signing in. When the DOM Package has an authenticated customer session, the catalog also includes membership-gated services that the customer is allowed to book. Services that are inactive, outside their visibility window, or unavailable to the current customer are not returned.
Membership eligibility and service price are separate. A free or paid membership can grant permission to see or book a service, but the service remains paid unless a discount, store credit, or another pricing rule changes its effective price.
The catalog response contains:
| Field | Description |
|---|---|
bookingConfigId | Customer flow ID supplied to getFlowServices() |
flow | Public flow information: id, name, and optional description |
services | Services available to the current visitor |
categories | Flow categories and the service IDs assigned to each category |
catalogSections | Configured catalog sections with category and service IDs |
currency | Flow location currency, when configured |
Each category contains id, name, optional description, optional image,
optional images, serviceIds, and optional presetSection. images is the
category's gallery in display order (up to 15), and image is its first entry.
Each catalog section contains id, name, categoryIds, and serviceIds.
Calendar-first service browsing
Use getFlowAvailability(bookingConfigId, options) to show a calendar first,
then show the services available when the visitor clicks a date. Your
application renders the calendar and the filtered service cards.
This method is available in DOM Package 1.7.1 and newer and returns
Promise<TiquoFlowAvailability>. The options type is
GetFlowAvailabilityOptions.
const summary = await tiquo.getFlowAvailability('CUSTOMER_FLOW_ID', {
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]),
);
// Render summary.days as calendar buttons. Disable unavailable days.
// Call this from a date button's click handler to get its service cards.
function servicesForDate(selectedDate) {
const day = summary.days.find((day) => day.date === selectedDate);
return (day?.serviceIds ?? [])
.map((id) => servicesById.get(id))
.filter(Boolean);
}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
// 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, party size and length to embedServiceBooking() so the customer doesn't
choose them again. See
Carry the customer's choices into the booking.
The embedded flow re-checks availability, and the customer can still change
anything.
Service fields
Each item in catalog.services uses the TiquoFlowService type:
| Field | Description |
|---|---|
id | Service ID used by getFlowService() and embedServiceBooking() |
name | Service name |
slug | Optional service slug |
description | Full service description |
shortDescription | Optional short description |
tags | 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 | The service's image gallery in display order (up to 15). The first entry is the primary image |
categoryImage | Primary image of the first flow category containing the service |
categoryImages | Primary image of each flow category containing the service. For a category's full gallery, use catalog.categories[].images |
categoryName | Name of the first flow category containing the service |
categoryNames | Names of all flow categories containing the service |
categoryDescription | First configured description from a flow category containing the service |
categoryDescriptions | Configured descriptions from all flow categories containing the service |
price | Base service price |
effectivePrice | Current effective price after applicable flow pricing |
currency | Currency code, when configured |
priceType | Service price type |
serviceType | Service type |
duration | Configured service duration |
durationType | Duration unit or booking type |
capacity | Most guests one booking can hold: the largest room or table that seats one party. Omitted when nothing limits a single booking, such as classes and events that sell spots from a shared pool. Not the partySize you searched with, and not the spots left |
location | Where the service is booked: id, name, sublocationId, sublocationName, and optional address, city, state, postalCode, country, coordinates (lat, lng), and timezone. A sublocation's own address replaces the venue address when one is set |
isFeatured | Whether the service is featured |
paymentType | Configured payment type |
depositRequired | Whether the service requires a deposit |
depositAmount | Configured deposit amount |
depositType | Configured deposit type |
minAdvanceBooking | Minimum advance-booking rule |
maxAdvanceBooking | Maximum advance-booking rule |
checkInTime | Check-in time, when configured |
checkOutTime | Check-out time, when configured |
minStay | Minimum stay, when configured |
maxStay | Maximum stay, when configured |
bookingLength | Length choice, when the customer chooses how long to book. See Booking length configuration |
categoryIds | IDs of the flow categories containing the service |
Use image for a ready-to-display image with automatic category fallback.
Use serviceImage or categoryImage when the UI needs to distinguish where
the image was configured. images remains service-only, while
categoryImages contains the categories' primary image URLs. categoryName is the first
associated flow category, while categoryNames contains every associated
category name. categoryDescription is the first configured description from
an associated category, while categoryDescriptions contains all configured
associated category descriptions.
Image galleries
Services and categories each hold an ordered gallery of up to 15 images.
Show the whole gallery as a carousel where there is room, such as a detail
page or hero card, and only the first image in compact layouts such as
thumbnails or list rows. TiquoFlowCategory.images and the service
location field are typed in DOM Package 1.11.0 and newer; older API
responses may omit a category's images, so fall back to image.
const catalog = await tiquo.getFlowServices('CUSTOMER_FLOW_ID');
for (const category of catalog.categories) {
const gallery = category.images ?? (category.image ? [category.image] : []);
renderCategoryHero(category.name, gallery);
}
for (const service of catalog.services) {
renderServiceCard(service.name, service.images[0] ?? service.image);
console.log(service.location?.name, service.location?.postalCode);
}Load one service
Use getFlowService() on a detail page:
const service = await tiquo.getFlowService(
'CUSTOMER_FLOW_ID',
'SERVICE_ID'
);
if (!service) {
// The service does not exist in this flow or is unavailable to this visitor.
}The method returns null when the service is not in the flow catalog visible
to the current visitor. It does not fall back to a different service.
Embed booking for the selected service
After rendering your custom service content, call embedServiceBooking():
<div id="service-booking"></div>await tiquo.embedServiceBooking(
'CUSTOMER_FLOW_ID',
service.id,
'#service-booking',
{
width: '100%',
minHeight: '240px',
autoResize: true,
}
);The embed:
- Preselects the exact service before the flow renders
- Removes the category and service-catalog navigation
- Prevents the customer from returning to select another service
- Keeps the flow's normal date, quantity, availability, payment, and booking steps
- Reuses the DOM Package's secure authenticated iframe handoff
- Automatically resizes unless
autoResizeis set tofalse
If the service ID is missing, invalid, or unavailable to the current customer, the embedded flow does not fall back to the full service catalog.
Carry the customer's choices into the booking
If your page already asked for the day, time, number of people or booking
length (for example with getFlowAvailability()), pass them on so the
customer doesn't choose them again:
await tiquo.embedServiceBooking('CUSTOMER_FLOW_ID', service.id, '#service-booking', {
date: '2026-10-14', // a days[].date, venue-local YYYY-MM-DD
startTime: '14:00', // a days[].slots[].time on that date, HH:mm
partySize: 6, // the partySize you searched with
lengthMinutes: 120, // for a service where the customer chooses the length
});| Option | Description |
|---|---|
date | Venue-local date, YYYY-MM-DD. The calendar opens on it and selects it while it's bookable |
startTime | Start time on date, HH:mm. Selected only while it's still free for the booking length and party size; otherwise the customer sees that day's remaining times. Requires date |
partySize | People in the booking, 1 to 1000. Sets the flow's people stepper (for example "Number of Attendees"), kept within the flow's minimum and maximum. Flows that sell a quantity of tickets instead ignore it |
lengthMinutes | Booking length in minutes, for a bookingLength range |
lengthOptionId | One of the service's bookingLength.options[].id |
A pre-selected time behaves like the customer's own click: flows set to move on automatically continue to the details step. Everything stays editable, and availability is checked again when the customer books.
date, startTime and partySize need DOM Package 1.11.0 or newer. Values
in the wrong format throw a TiquoAuthError with code
INVALID_SERVICE_BOOKING_START (date or time) or INVALID_PARTY_SIZE.
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.
<div id="category-booking"></div>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,
});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.
Custom catalog and detail page pattern
A typical integration uses two application routes:
- The catalog route calls
getFlowServices()and renders a card or block for each service. - Each card links to a custom detail route with the service ID in the route or query string.
- The detail route calls
getFlowService()and renders the service's image, description, price, and other fields. - The detail route calls
embedServiceBooking()inside the booking section.
This keeps discovery and page design fully controlled by your application while Tiquo handles the final availability and booking journey.
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.