Search availability and book an appointment
Build a flow that lets a customer pick a treatment, find a time, and walk that selection into a basket that is ready to check out. This guide takes you from offering discovery through to the moment the basket hand-off begins; the basket and checkout guide picks up where this one ends.
Before you start
- A personal access token for the playground environment with read access to the site you're integrating with. See authentication for the header format.
- A
site_idfor the venue. List the sites your token can see by callingGET /sites— every other request in this guide is scoped to one of them. - At least one bookable offering on that site — an appointment type, a session, a package, or an area booking. The sample tenant in the playground ships with a handful of each.
- A timezone-aware HTTP client. Every date and time in the API uses ISO 8601 with an explicit offset; you should send the same.
The shape of the flow
Trybe models a booking as four objects:
- An offering — the thing the customer wants (an
AppointmentType, aPackage, aSession, anAreaBookingType). - A slot — a concrete time the offering can be performed, with a practitioner or a room attached. Slots are computed on demand and never persisted on their own.
- A basket item — a slot that has been reserved against a basket. This is the first object that holds the resource for the customer.
- An order — the result of submitting the basket. Everything past this point is fulfilment and reporting.
The five-step path through this guide is: discover offerings, find dates, find slots, create the basket and add the slot, then hand off to the checkout flow.
Two things to internalise before you write any code against this surface:
Slots are ephemeral. When you call any of the slot endpoints you get back the slots that are bookable right now, computed against practitioner rotas, existing appointments, room schedules, blocked time, and price rules. Two requests three seconds apart can return different results. Treat the response as a render-time snapshot — never persist a slot list and reuse it later.
The offering-type discriminator drives everything downstream. Every
endpoint that accepts an offering_id also wants an offering_type,
because IDs are not globally unique across types and the two together
form the key the API looks up. The valid values are appointment,
appointment_enquiry, area_booking, course, hotel_room_reservation,
membership, package, product, session, table_reservation, and
voucher. The first six of those have their own availability flow; the
rest either don't need one (products) or live in a separate guide
(memberships, vouchers).
1. Discover bookable offerings
Use the offering search endpoint to populate a "what can I book?" list.
It returns a unified Offering envelope across every bookable type, which
is normally what a storefront wants — one list, one card design.
curl https://api.playground.try.be/shop/offering-search \
--get \
--data-urlencode "site_id=00000000-0000-0000-0000-111111111111" \
-H "Authorization: Bearer $TRYBE_API_KEY" \
-H "Accept: application/json"
The response is a paginated data array of offerings; each one carries the
fields you need to make the next call:
{
"data": [
{
"id": "5e932c0901d210625e3a8766",
"type": "appointment",
"name": "Deep Tissue Massage",
"duration_minutes": 60,
"price": 9500,
"currency": "GBP"
}
],
"meta": { "current_page": 1, "total": 24, "per_page": 15, "last_page": 2 }
}
The id and type together form the discriminated key you'll pass as
offering_id + offering_type in every subsequent call.
If you want a tighter list — for example a "Popular at this spa" rail —
use the most-popular variant. It returns the same Offering shape, ordered
by sales count over the trailing four weeks, and accepts a comma-separated
offering_type filter (prefix a value with - to exclude it).
curl "https://api.playground.try.be/shop/offering-search/most-popular?site_id=00000000-0000-0000-0000-111111111111&offering_type=appointment,package&per_page=6" \
-H "Authorization: Bearer $TRYBE_API_KEY" \
-H "Accept: application/json"
For offering-type-specific pickers you can also call the dedicated lists:
listAppointmentTypes,
listSessionTypes, and
listPackages. These are useful when
you need fields that are specific to one offering shape (e.g. session
capacity, package choices) and aren't exposed on the unified
Offering envelope.
See listOfferings and
listMostPopularOfferings
for the full parameter list.
2. Narrow to a date
Most storefronts paint a calendar before they fetch slots — greying out days where nothing is available so the customer doesn't tap into an empty list. Two endpoints serve that view, and which one you reach for depends on the offering type.
For a quick calendar across any offering type, use the per-day endpoint.
It supports appointment, session, package, and area_booking, takes
a date_from..date_to window of up to 31 days, and returns one entry
per day with is_available and the cheapest slot price.
curl https://api.playground.try.be/shop/item-availability/offering-dates \
--get \
--data-urlencode "date_from=2026-06-01" \
--data-urlencode "date_to=2026-06-30" \
--data-urlencode "offering_id=5e932c0901d210625e3a8766" \
--data-urlencode "offering_type=appointment" \
-H "Authorization: Bearer $TRYBE_API_KEY" \
-H "Accept: application/json"
const params = new URLSearchParams({
date_from: '2026-06-01',
date_to: '2026-06-30',
offering_id: '5e932c0901d210625e3a8766',
offering_type: 'appointment',
})
const res = await fetch(
`https://api.playground.try.be/shop/item-availability/offering-dates?${params}`,
{
headers: {
Authorization: `Bearer ${process.env.TRYBE_API_KEY}`,
Accept: 'application/json',
},
},
)
const { data } = await res.json()
const bookableDays = data.filter((d) => d.has_availability)
If you ask for a range longer than 31 days the API returns 400. Walk the
calendar one month at a time and cache the result on the client; you'll
need to re-fetch shortly before you reserve a slot anyway.
For a monthly "is the offering generally bookable?" view that doesn't
require fully resolving slots, use the OfferingAvailability endpoint.
It returns one entry per calendar day in the month containing date,
honouring every available rule on the offering minus the unavailable
overrides.
curl https://api.playground.try.be/shop/offering-availability \
--get \
--data-urlencode "date=2026-06-15" \
--data-urlencode "offering_id=5e932c0901d210625e3a8766" \
--data-urlencode "offering_type=appointment" \
-H "Authorization: Bearer $TRYBE_API_KEY"
See
listOfferingDateAvailability
and
listOfferingAvailability
for the full parameter and response shapes.
The legacy
listAppointmentAvailableDates
endpoint is still present for compatibility with older storefronts but is
deprecated — new integrations should use the unified offering-dates
endpoint above.
3. Resolve a slot
Once the customer has picked a day, fetch the slots for that day. Slots are where the rubber meets the road — each one represents one concrete combination of start time, practitioner (or room, or area), and capacity unit. The slot endpoints honour every constraint that affects bookability: practitioner rotas, room schedules, blocked time, capacity caps, lead-time rules, and price rules. They never return an unbookable slot.
The price embedded in each slot is the price the customer will be quoted at checkout, including any time-of-day or day-of-week price rules that apply. Don't compute prices on the storefront from the offering's headline price; surface the slot price instead.
For appointments you call the range endpoint, scoped to one AppointmentType
at one site:
curl "https://api.playground.try.be/shop/item-availability/appointment-slots/00000000-0000-0000-0000-111111111111/5e932c0901d210625e3a8766" \
--get \
--data-urlencode "date_time_from=2026-06-15T09:00:00+01:00" \
--data-urlencode "date_time_to=2026-06-15T21:00:00+01:00" \
-H "Authorization: Bearer $TRYBE_API_KEY" \
-H "Accept: application/json"
$response = Http::asJson()
->withToken(config('services.trybe.token'))
->get('https://api.playground.try.be/shop/item-availability/appointment-slots/'
. $siteId . '/' . $appointmentTypeId, [
'date_time_from' => '2026-06-15T09:00:00+01:00',
'date_time_to' => '2026-06-15T21:00:00+01:00',
]);
$slots = $response->json('data');
Each entry includes the slot's start and end time, the eligible
practitioners, current capacity, and the per-slot price (which may differ
from the offering's headline price if a PriceRule is in force for the
date or time). Ranges of a day or less run a live availability check;
longer ranges fall back to the cached schedule, so for the customer-facing
flow keep the window tight.
If you're letting the customer pick a practitioner, narrow the slot list
with practitioner_id. The catalogue of practitioners for the site is
served by
listAllPractitioners —
note the name; there is no listPractitioners operation.
For sessions, the equivalent slot listing lives at
/shop/item-availability/sessions/{siteId}/{sessionTypeId}; for area
bookings, at /shop/item-availability/area-booking-slots/{areaBookingTypeId}.
Both follow the same date_time_from / date_time_to convention. See
listAppointmentSlotsByRange
for the appointment shape, which doubles as the model for the others.
Packages
Packages are a special case because they're composed of one or more
"choices" — each choice is a slot in its own right. To fetch availability
for a package you ask for slots one choice at a time, using
/shop/item-availability/package-slots/{siteId}/{packageId} (for a
package-level view) or
/shop/item-availability/package-slots/{packageId}/choices/{choiceId}
(for a single choice). On the basket side, you reserve the parent item
first and then add one nested package-item per configured choice; the
basket-and-checkout guide walks through it.
Practitioners and rooms
If your storefront lets the customer choose who performs the treatment, list the practitioners for the site and use the resulting IDs to filter slots.
curl "https://api.playground.try.be/shop/practitioners?site_id=00000000-0000-0000-0000-111111111111" \
-H "Authorization: Bearer $TRYBE_API_KEY" \
-H "Accept: application/json"
You can also fetch the configured display order with
listPractitionersOrder
so your UI matches what the venue sees in the back office. Rooms have an
equivalent listRooms — supply
room_id on the basket item when the customer or operator has chosen a
specific room.
Pre-flight checks for appointment enquiries
Some appointment types are configured as "enquiry-only" — they don't resolve to a hard slot and are accepted by the venue manually. For these, use the enquiry availability check before adding to the basket:
curl "https://api.playground.try.be/shop/item-availability/appointment-enquiries/5e932c0901d210625e3a8766" \
--get \
--data-urlencode "date_time_from=2026-06-15T14:00:00+01:00" \
--data-urlencode "date_time_to=2026-06-15T15:00:00+01:00" \
-H "Authorization: Bearer $TRYBE_API_KEY"
A 200 indicates the enquiry can be created; a 422 reports why it can't.
See
actionCheckAppointmentEnquiryAvailability.
4. Create the basket
You have an offering, a date, and a chosen slot. Create a basket against the site so you have somewhere to put the reservation:
curl https://api.playground.try.be/shop/basket \
-X POST \
-H "Authorization: Bearer $TRYBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "site_id": "00000000-0000-0000-0000-111111111111" }'
The response is a fresh Basket with an id and an empty items array.
That basket_id is the only thing you need to keep around for the next
step — the rest of the basket flow is bound to it. If you already know
the customer, pass customer_id in the body; otherwise you can attach the
customer later with actionSetBasketCustomer.
See createBasket for the full request
body.
5. Add the slot to the basket
The handoff from "we found a slot" to "we hold the slot for the customer" happens by adding a basket item with the offering identifiers plus the chosen date/time. The exact field set varies by offering type, but the common envelope is:
curl "https://api.playground.try.be/shop/basket/$BASKET_ID/items" \
-X POST \
-H "Authorization: Bearer $TRYBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"offering_id": "5e932c0901d210625e3a8766",
"offering_type": "appointment",
"date_time": "2026-06-15T14:00:00+01:00",
"guests": [
{ "name": "Jane Doe", "is_lead_booker": true }
]
}'
const res = await fetch(
`https://api.playground.try.be/shop/basket/${basketId}/items`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TRYBE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
offering_id: '5e932c0901d210625e3a8766',
offering_type: 'appointment',
date_time: '2026-06-15T14:00:00+01:00',
guests: [{ name: 'Jane Doe', is_lead_booker: true }],
}),
},
)
if (res.status === 409) {
// Slot taken between fetch and add — re-fetch slots and prompt again.
}
const basket = (await res.json()).data
The response is the full Basket with the new item attached. From here
the basket-and-checkout flow takes over: assign guests, attach the paying
customer, apply discounts, take payment, and submit.
For packages, swap createBasketItem for
createBasketPackageItem at
the nested package-items URL — the structure is the same, but you need
one package item per configured choice. The basket-and-checkout guide
walks through it in detail.
See createBasketItem for the
full request shape and the additional fields available per offering type.
Going further
- Build a basket and check out — the next step. Reserve, pay, and submit.
- Booking frame — embed a Trybe-hosted booking UI instead of building your own; reach for this when you want to skip steps 1–5 entirely.
- Webhooks — subscribe to
basket.submittedand related events so your back-end systems can react asynchronously. - Endpoint references:
listOfferings,listOfferingDateAvailability,listAppointmentSlotsByRange,createBasket,createBasketItem.
Putting it together
A typical storefront flow chains the five steps into a single page-level
state machine. On first load it calls listOfferings and renders the
catalogue. When the customer picks an offering it calls
listOfferingDateAvailability for the next month and paints the
calendar. When the customer picks a day it calls
listAppointmentSlotsByRange for that day and renders the slot list.
When the customer picks a slot it calls createBasket (if one doesn't
already exist for the session) and then createBasketItem with the slot
co-ordinates. From there the basket-and-checkout flow takes over.
The catalogue and calendar calls can run in parallel with the rest of the page; the slot call should run only when the customer commits to a day (it's the most expensive of the four). The basket calls should be sequential and gated behind explicit customer intent — never reserve a slot speculatively, or you'll create lock contention with no resulting order.
Production checklist
- Re-check availability before reserving. Slot lists are point-in-time
snapshots. Always re-fetch slots immediately before calling
createBasketItem; if another booking lands first you'll receive a409 Conflict. Surface a "this time was just taken" message and prompt the customer to pick again. - Send timezone-aware date-times. Use ISO 8601 with an explicit offset
(
+01:00, notZ, and never a naive14:00:00). The API stores and returns offsets so a slot at 14:00 local time is unambiguous across daylight-saving transitions. - Cap calendar queries at 31 days.
listOfferingDateAvailabilityrejects wider ranges. Walk the calendar one month at a time and cache the result client-side until the customer commits to a day. - Handle the
409fromcreateBasketItem. Treat it as a soft error, not an exception. Re-fetching slots and prompting the customer again is always the right response. - Retry only idempotent reads. The availability endpoints are safe to
retry with exponential backoff on
5xx. The basket-write endpoints are not — see the basket-and-checkout guide for the idempotency story. - Keep slot lookups close to the reservation. A slot list fetched
five minutes ago is already stale enough to cause
409s during a busy window. For high-traffic venues, refetch when the customer focuses the "book" button rather than only on page load. - Respect the practitioner permissions model. A token whose user cannot view a practitioner's rota will see no slots for that practitioner. If a venue reports "I see availability in the app but the API doesn't", check the integration user's permissions before assuming a data issue.