Turn a bookable product into a reservation: list offerings, check availability, optionally pick a resource, and create a booking. Previous: Guide 18 | Next: Guide 20
Reference - This guide is a tutorial. For exhaustive, always-current signatures and field lists, see the auto-generated reference (it regenerates on every
docs:sync, so it never goes stale):
Types: SDK Types
API: Storefront: Commerce
Prerequisites
Lynkow SDK installed (Quick Start)
A publishable key on the client (see Guide 18). Reservation endpoints reject a call without one.
A bookable product. Reservation offerings and availability hang off a product whose
kindis'bookable'. Find one withlynkow.products.list({ kind: 'bookable' })orlynkow.products.getBySlug(...)from Guide 18.
import { createClient } from 'lynkow'
const lynkow = createClient({
siteId: '550e8400-e29b-41d4-a716-446655440000',
publishableKey: 'lkw_pk_9f8g7h6j5k4w3m2n1p0qrstv',
})The always-available reservation flow is three steps: read the product's offerings and availability, then create a booking. That booking is confirmed immediately. Payment is taken on site, in person. See "Payment" at the end for how online deposits fit in.
Step 1: List offerings (optional)
Some bookable products expose several formulas (for example a 60-minute cut versus a 90-minute cut-and-color), each with its own price and duration. lynkow.reservations.offerings(productIdOrSlug) lists them in the merchant's display order. It is not cached, since a merchant can change a price or archive a formula at any time.
// Each offering (partial): { id, label, priceCents, durationMin }
const offerings = await lynkow.reservations.offerings('haircut')
for (const offering of offerings) {
const price = (offering.priceCents / 100).toFixed(2)
console.log(`${offering.label}: ${price} (${offering.durationMin} min)`)
}A product with no formulas returns an empty array, so a single-price product keeps a simple flow. When the array is non-empty and offering selection is enabled, the chosen offering.id becomes required on the availability query and the booking (see below).
Step 2: Check availability
lynkow.reservations.availability(productIdOrSlug, filters) returns the open slots across a date range. from and to are calendar dates (YYYY-MM-DD) and to must not precede from. Pass the selected offeringId when the product uses offerings, so the slot grid reflects that formula's duration.
// Each slot (partial): { startsAt, endsAt, available, seatsRemaining }
const { slots, timezone } = await lynkow.reservations.availability('haircut', {
from: '2026-07-10',
to: '2026-07-17',
offeringId: offerings[0]?.id, // omit for a single-price product
})
for (const slot of slots) {
if (slot.available) {
console.log(`${slot.startsAt} - ${slot.seatsRemaining} seat(s) left`)
}
}
console.log(`Times shown in ${timezone}`)Availability is never cached: it changes as other buyers book. Read each slot's seatsRemaining to decide what to offer. The response also carries a window whose to may be earlier than requested when the server clamps an over-wide span.
Step 3: Pick a resource (optional)
When the merchant runs the product in customer_optional mode with a resource pool (for example specific stylists or rooms), lynkow.reservations.resources(productIdOrSlug) lists the resources a buyer may choose from. In auto-assign mode, or for a product with no pool, it returns an empty array and you render no picker.
// Each resource (partial): { id, name }
const resources = await lynkow.reservations.resources('haircut')
for (const resource of resources) {
console.log(`${resource.name} (${resource.id})`)
}Pass the chosen resource.id as resourceId on the booking to request that specific resource. Omit it to let the server auto-assign a free one.
Step 4: Create the booking
lynkow.reservations.bookings.create(productIdOrSlug, data) creates the reservation. No account is required: a guest supplies their own name and email. The returned booking is confirmed right away, because payment is handled on site.
// Booking (partial): { reference, status, startsAt, endsAt, partySize }
const booking = await lynkow.reservations.bookings.create('haircut', {
startsAt: slots[0].startsAt, // copied from an available slot
partySize: 1,
guestName: 'Alice Martin',
guestEmail: '[email protected]',
guestPhone: '+33 6 12 34 56 78', // optional
offeringId: offerings[0]?.id, // required when the product uses offerings
resourceId: resources[0]?.id, // optional; omit to auto-assign
})
console.log(`Reservation ${booking.reference} is ${booking.status}. Payment is taken on site.`)The SDK injects anti-spam honeypot and timestamp fields automatically. Two consequences to handle on a long-lived or non-browser client: a booking sent within a few seconds of createClient(...) is rejected 'TOO_MANY_REQUESTS' (too fast), and one sent more than roughly an hour later is rejected 'BAD_REQUEST' (form expired). On a storefront that enforces reCAPTCHA rather than the default honeypot, pass options.recaptchaToken.
Booking errors
import { isLynkowError } from 'lynkow'
try {
await lynkow.reservations.bookings.create('haircut', { /* ... */ })
} catch (error) {
if (isLynkowError(error) && error.code === 'VALIDATION_ERROR') {
// Bad input, or the slot filled between the availability read and this call.
// Re-fetch availability and let the buyer pick again.
} else {
throw error
}
}'VALIDATION_ERROR' (HTTP 422) also covers a slot that filled after you read availability, so re-fetching slots on this error is the right recovery.
Attach a booking to a signed-in buyer
When the same client already holds a customer session (see Guide 20), booking creation attaches it automatically, and the booking then shows up in lynkow.customers.me() right away. For a signed-in buyer the recorded email is always the account email, and the name is the stored account name once their email is verified, so both can differ from what a form submitted. Guest bookings are unchanged when no session is present.
Payment
Today the live, always-available payment model for a reservation is on site: the booking above is confirmed immediately and the buyer pays in person.
An online deposit (a card payment taken to hold the booking) is a separate, per-site capability. It may be enabled for your site, or it may be coming. Treat it as optional: do not wire a checkout redirect into your default booking flow.
When your site has the deposit capability enabled for a product,
lynkow.reservations.bookings.create(...)throws'CONFLICT'(HTTP 409) instead of silently returning an unpaid booking, because that flow only ever returns aconfirmedreservation.lynkow.reservations.book(...)is the deposit-aware entry point for those products: it returns either the same confirmed booking or apendingreservation carrying the deposit details.lynkow.reservations.refreshPayment(...)exists to reconcile a deposit's status after the buyer returns from checkout. It is only relevant once the deposit capability is active for your site.
If the deposit capability is not enabled for your site, you do not need book() or refreshPayment() at all: bookings.create() is the complete flow. Check the current commerce capability and payment shapes in the Storefront: Commerce reference before building an online-payment step.
Reference
Do not treat this guide as the field list. For every method, signature, and property, link the source of truth:
SDK services:
ReservationsService,ProductsService- every method and signature.Types: SDK Types - the full
AvailabilityResponse,ReservationOffering,ReservationResource, andBookingshapes.API: Storefront: Commerce - request and response schemas, and the current payment capability.
Next Steps
Guide 20: Customer Accounts - let buyers sign up, sign in, and see their own bookings.