Guide

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_id for the venue. List the sites your token can see by calling GET /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:

  1. An offering — the thing the customer wants (an AppointmentType, a Package, a Session, an AreaBookingType).
  2. 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.
  3. A basket item — a slot that has been reserved against a basket. This is the first object that holds the resource for the customer.
  4. 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

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 a 409 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, not Z, and never a naive 14: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. listOfferingDateAvailability rejects 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 409 from createBasketItem. 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.