Skip to main content
This tutorial walks through building a custom booking page on the Opencals Storefront API — an API for booking systems that handles services, real-time availability, carts, checkout, and payments. We’ll use Next.js and the typed @opencals/storefront-sdk, but every step maps 1:1 to plain REST calls, so the same 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

For salons and similar businesses, customers often 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:
A few things to know about add-on pricing:
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.
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 June 21, 2026