Skip to main content
This tutorial walks through building a custom booking page on the Opencals Storefront API. We’ll use Next.js and the typed @opencals/storefront-sdk, but every step maps 1:1 to plain REST calls — the flow works from any language. The complete booking flow is eight API calls:
  1. List services — GET /storefront/products
  2. Pick a staff member — GET /storefront/staff-members
  3. Fetch available time slots — GET /storefront/products/{productId}/current-availabilities
  4. Create an appointment — POST /storefront/appointments
  5. Add it to a cart — POST /storefront/cart and POST /storefront/cart/items
  6. (Optional) Show and attach add-ons — GET /storefront/products/{productId}/add-ons and POST /storefront/cart/add-ons
  7. (Optional) Apply a promo code — POST /storefront/cart/discount-code and DELETE /storefront/cart/discount-code
  8. Check out — POST /storefront/cart/checkout/start and POST /storefront/cart/checkout/submit

Setup

Install the SDK in a Next.js project and initialize it once, for example in a small module imported by your server code:
lib/opencals.ts
The API key must stay server-side, so the calls below belong in server components, route handlers, or server actions — not client components.
1

List services

ProductService.list() wraps GET /storefront/products and returns a paginated list of active services:
A few product fields drive your UI:
2

Let the customer pick a staff member

If customers choose who they book with, StaffMemberService.list() wraps GET /storefront/staff-members:
The selected staff member’s id filters availability in the next step. The same pattern works for locations via LocationService.list() (GET /storefront/locations) on multi-location stores.
3

Fetch available time slots

ProductService.getCurrentAvailabilities() wraps GET /storefront/products/{productId}/current-availabilities. It takes a required date (YYYY-MM-DD) plus optional staffMemberId, locationId, and timezone filters:
Render the returned slots as your time picker.
For custom-duration products (isCustomDuration: true, e.g. rentals or venue hire), use GET /storefront/products/{productId}/current-availability-ranges and show a date-range picker instead. There’s also GET /storefront/products/{productId}/nearest-availability for “next available slot” badges on service cards.
4

Create the appointment

When the customer picks a slot, create an appointment with POST /storefront/appointments. The required slot object carries the product and the UTC date/time range; staffMemberId and locationId are optional:
All slot date/times are UTC — convert from the customer’s timezone before sending.
5

Cart

Carts hold appointments between slot selection and payment, and they expire after a few minutes so abandoned selections release their slots. Cart endpoints identify the cart with the X-Cart-Id header:
Persist cart.id (for example in a cookie) so the customer keeps their cart across pages. If checkout takes a while, POST /storefront/cart/extend adds five minutes to the cart’s expiration.
6

Show and attach add-ons (optional)

If the service has add-ons (extras like equipment rentals, upgrades, or accessories), fetch them and let the customer choose before proceeding to payment.
When the customer selects an add-on, attach it to the cart item:
Add-on pricing fields:
Skip this step if addOns.data is empty — not every service has add-ons configured.
7

Apply a promo code (optional)

If the store offers discount codes, let customers enter a promo code before checkout. CartService.applyDiscountCode() wraps POST /storefront/cart/discount-code:
Remove a code with DELETE /storefront/cart/discount-code:

Reading applied discounts

Every cart-returning endpoint now includes appliedDiscounts[] and per-line-item discounts[] fields. Show a “You saved $X” summary by subtracting the cart’s total from the sum of original line prices:
8

Checkout

Checkout is a three-call sequence against the same X-Cart-Id.Start validates the cart and initializes payment with your chosen provider (list configured providers with GET /storefront/payment/providers):
Save the customer so the order and appointments have contact details (guests don’t need an account):
If the store has custom checkout questions, fetch them with GET /storefront/cart/checkout/questions/{language} and save answers via POST /storefront/cart/checkout/save-answers.Submit captures the payment and creates the order:
The response includes the created order and, for new customers, an auth token pair you can use to sign them in automatically — see Authentication.If the email belongs to an existing customer and the request isn’t signed in as them, the booking is still added to that customer’s account, but auth is omitted and their saved profile (name, phone, marketing consent) isn’t changed. The billing and delivery addresses entered are added to the customer’s saved addresses once the order is placed; save-customer and checkout/start don’t write them. The customer sees the booking after signing in (for example with a login code). Always treat auth as optional.
For pay-at-venue stores, start checkout with provider: "cash" and submit directly with no payment confirmation step.

After the booking

With a customer token you can power a full self-service portal:
  • GET /storefront/appointments — list the customer’s appointments
  • PUT /storefront/appointments/{appointmentId}/reschedule — move a booking
  • PUT /storefront/appointments/{appointmentId}/cancel — cancel it
  • GET /storefront/orders — order history with payments and refunds

Skip the boilerplate

If you’d rather start from working code, Opencals publishes free open-source Next.js booking templates built on this exact flow — browse them at open-source/templates or clone the Haar salon template. For the full endpoint catalog, head to the API reference.
Last modified on October 18, 2026