"
```
### All auth endpoints
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/storefront/auth/sign-up` | Register a new customer (sends a verification email) |
| `POST` | `/storefront/auth/sign-in` | Sign in with email and password |
| `POST` | `/storefront/auth/oauth` | Sign in or register via an OAuth provider |
| `GET` | `/storefront/auth/refresh` | Refresh tokens (pass the refresh token as Bearer) |
| `POST` | `/storefront/auth/request-email-verification` | Resend the verification email |
| `POST` | `/storefront/auth/verify-email` | Verify an email with the emailed token |
| `POST` | `/storefront/auth/request-password-reset` | Send a password reset email |
| `POST` | `/storefront/auth/reset-password` | Reset a password with the emailed token |
See the [API reference](/docs/api-reference/introduction) for full request and response shapes.
### Which endpoints need a customer token?
Customer-scoped endpoints — appointments, orders, and self-service profile management — require a customer Bearer token. Catalog endpoints (products, availability, staff members, locations, collections, store settings) need only the API key.
Cart and checkout endpoints accept an *optional* customer token: guests can book without an account, and checkout can return auth tokens for new customers so they're signed in automatically after their first booking. Checkout never returns tokens for an existing customer identified only by the email in the request; that customer signs in themselves to see the booking.
## Error responses
Errors use conventional HTTP status codes with a JSON body:
```json theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{ "property": "email", "messages": ["Email is required"] }
]
}
```
| Status | Meaning |
| - | - |
| `400` | Validation error — check the `errors` array for per-field details |
| `401` | Invalid or missing API key or customer token |
| `404` | Resource not found |
| `429` | Rate limit exceeded — see [rate limits](/docs/quickstart#rate-limits) |
## Security checklist
* Store `sfk_` keys in environment variables, never in client bundles or repositories.
* Call the Storefront API from your server (API routes, server components, edge functions) and keep the key out of browser network requests.
* Store customer refresh tokens in `httpOnly` cookies rather than `localStorage` where possible.
* Rotate API keys from the dashboard if a key is ever exposed.
# Changelog
Source: https://opencals.com/docs/changelog
What's new in the Opencals developer platform — Storefront API, TypeScript SDK, open-source templates, and developer docs.
Stay up to date with releases across the Opencals developer platform.
Subscribe to releases on [GitHub](https://github.com/letsopencals) to get notified.
Each release is its own page in the sidebar, grouped by month. The
[Storefront SDK](/docs/changelog/storefront-sdk) page tracks every version so you
can see exactly which release a feature first shipped in.
## October 2026
October 18 · API — no tokens or profile changes for an existing customer named only by email; addresses entered for that booking are saved to the customer once the order is placed.
October 12 · Template + API — open-source villa-rental site with nightly stays, a `villa_resort` seed dataset, and fixed check-in/check-out times enforced on customer bookings.
October 9 · Breaking · SDK v0.3.17 — staff `email` and `userId` are no longer in Storefront API responses or types.
October 9 · Template — open-source language-school site and a `language_school` seed dataset with online classroom links.
October 7 · Template + fix — open-source luxury car-rental site, a `car_rental` seed dataset, and correct validation for bookings longer than 24 hours.
## September 2026
September 27 · Deprecation · SDK v0.3.17 — use `appointment.order` instead of `externalOrderId` / `externalOrderName`.
September 20 · Template — open-source padel & squash club booking site, with a multi-court grid and custom durations.
## August 2026
August 15 · Template — open-source medical clinic booking site.
August 1 · New feature · SDK v0.3.11 — attach arbitrary key-value data to appointments at booking time.
## June 2026
June 21 · New feature · SDK v0.3.2 — automatic and promo-code discounts.
June 17 · Template — open-source barbershop booking site.
June 13 · New feature · SDK v0.3.0 — optional extras at booking time.
June 13 · Docs — API reference, guides, and the in-browser playground.
June 7 · Template — open-source salon booking site.
June 5 · API — the public customer-facing booking API.
Version history — the official TypeScript client, v0.1.0 → v0.3.2.
# Add-Ons
Source: https://opencals.com/docs/changelog/add-ons
Optional extras attached to any bookable service, selected during the booking flow and paid in one transaction. Storefront SDK v0.3.0.
## Add-Ons — new feature
Service businesses can now attach optional extras to any bookable service.
Customers select add-ons during the booking flow — after picking a service and
time slot, before checkout — and pay the combined total in one transaction.
**What's in the release:**
* `GET /storefront/add-ons` — list add-ons, filter by `productId`
* `GET /storefront/products/:id/add-ons` — add-ons for a specific service
* `POST /storefront/cart/add-ons` — add an add-on to a cart item
* `PATCH /storefront/cart/add-ons/:id` — update quantity
* `DELETE /storefront/cart/add-ons/:id` — remove from cart
**Key behaviors:**
* Add-ons are scoped to products. Only add-ons linked to the booked service appear.
* Two pricing modes: **fixed** (customer picks quantity) and **duration-multiplied** (quantity auto-set to the number of duration units, useful for per-day or per-hour charges).
* Prices are snapshotted at cart creation — later catalog changes don't affect existing orders.
* Add-ons appear in order line items (`OrderAddOnLineItem`) and on appointment records (`AppointmentAddOn`). Partial quantity refunds are supported.
* Works on both `platform='opencals'` and `platform='shopify'`.
**SDK support** — available in `@opencals/storefront-sdk`
[v0.3.0](/docs/changelog/storefront-sdk) and higher (via `AddonService`). Older
versions do not include add-on methods or types.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
# List add-ons for a service
curl -H "X-Api-Key: sfk_your_key" \
https://api.opencals.com/storefront/products/{productId}/add-ons
# Add to cart
curl -X POST -H "X-Api-Key: sfk_your_key" \
-H "X-Cart-Id: {cartId}" \
-H "Content-Type: application/json" \
-d '{"cartItemId": "...", "addOnId": "...", "quantity": 1}' \
https://api.opencals.com/storefront/cart/add-ons
```
See the [Add-Ons feature page](https://opencals.com/features/add-ons) for the
full overview and use cases.
# AI Agent Skills
Source: https://opencals.com/docs/changelog/ai-agent-skills
Opencals Agent Skills — ground-truth Skills that teach Claude Code, Cursor, Codex and Gemini CLI to build booking integrations on the Opencals API and SDK, plus a new AI Agents docs section and machine-readable llms.txt.
## Agent Skills for building on Opencals
Most integrations are now written inside an AI coding agent. So we shipped
**Agent Skills** that give those agents ground-truth knowledge of the Opencals
API and SDK — real method names, argument shapes and flows, no guessing. They
follow the open [Agent Skills](https://code.claude.com/docs/en/skills) standard
and work in Claude Code, Cursor, OpenAI Codex, Gemini CLI and more.
Two skills, in one public repo
([github.com/letsopencals/skills](https://github.com/letsopencals/skills)):
* **`opencals-storefront-api`** — the ground-truth reference: availability (slots
and ranges, custom durations), carts and expiry, appointments, checkout
(addresses, marketing consent), customer accounts, guests and attendees,
discounts (automatic, codes, balance credit), reschedule/cancel, and rate
limiting.
* **`opencals-build-booking-site`** — builds a whole booking website: pick an
official template, scaffold, wire it up, rebrand, deploy.
Install in Claude Code:
```
/plugin marketplace add letsopencals/skills
/plugin install opencals@opencals
```
Also new:
* A dedicated **[AI Agents](/docs/ai-agents/overview)** section in the docs, with
per-tool install instructions (including v0 / Lovable / Bolt).
* Machine-readable docs at **[llms.txt](https://opencals.com/docs/llms.txt)** and
**[llms-full.txt](https://opencals.com/docs/llms-full.txt)** for agents that
consume docs directly.
[Skills repo ↗](https://github.com/letsopencals/skills) ·
[AI Agents docs](/docs/ai-agents/overview)
# Lovable, v0 and Base44
Source: https://opencals.com/docs/changelog/ai-app-builders
Setup guides for building Opencals booking sites in Lovable (custom connector), v0 (agent skills) and Base44 (backend function + workspace skills).
## Build booking sites in Lovable, v0 and Base44
Three new guides in the renamed **[AI & Builders](/docs/ai-agents/overview)** tab, each
with every field and file ready to copy:
* **[Lovable](/docs/ai-agents/lovable)**: add Opencals as a custom connector with
Advanced auth (`X-Api-Key` + `X-Api-Version`) and two knowledge files.
* **[v0](/docs/ai-agents/v0)**: attach `letsopencals/skills` from skills.sh and set
`OPENCALS_API_KEY`.
* **[Base44](/docs/ai-agents/base44)**: an `opencals` backend function, an app secret
and two workspace skills.
The knowledge files are also published as plain Markdown at
[opencals.com/ai/opencals-storefront-api.md](https://opencals.com/ai/opencals-storefront-api.md),
[lovable-booking-site-playbook.md](https://opencals.com/ai/lovable-booking-site-playbook.md) and
[base44-booking-site-playbook.md](https://opencals.com/ai/base44-booking-site-playbook.md).
# Bali Villa Rentals — villa booking template
Source: https://opencals.com/docs/changelog/bali-villas-template
Bali Villa Rentals, a free open-source villa and holiday-rental website template built on the Storefront API, plus a villa-resort seed dataset and server-side enforcement of fixed check-in and check-out times.
## Bali Villa Rentals — open-source villa booking template
The seventh open-source Opencals template is live, and the first one built
around **nightly stays**. **Bali Villa Rentals** is a villa-rental website for
a demo resort, Lumbung Sidemen: ten villas in the rice fields of East Bali. The
same [Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) as the other templates, with a warm,
quiet design and every nightly rate on show.
* **Booked by the night** — each villa is one staffless product with a one-night base duration, `allowCustomDuration`, and fixed check-in 15:00 / check-out 11:00, on a continuous 24/7 schedule
* **Calendar on live availability** — reads `getCurrentAvailabilitiesMerged` and checks on the client that check-in → check-out fits inside one range
* **Turnover days** — a check-out day can be the next guest's check-in day
* **Guest details as custom attributes** — adults, children, infants, arrival time, flight number and occasion
* Per-stay extras (airport transfer, floating breakfast, early check-in request), checkout questions and promo codes
* Trip-aware customer accounts: dates in the resort timezone, night count, change dates (same number of nights) or cancel up to 14 days before check-in
* Passwordless login and one-time email links
* `LodgingBusiness`, `VacationRental` and `FAQPage` structured data, Open Graph, `robots.txt` and `sitemap.xml` configured
* Stack: Next.js 15, TypeScript, Tailwind v4, Framer Motion, `@opencals/storefront-sdk` v0.3.17
* MIT licensed — clone, rebrand, deploy. One-click Vercel deploy.
### Villa-resort seed dataset
New Dev Stores can choose the **Bali Villa Rentals** dataset (category Villa
resort, `villa_resort`). It seeds ten villas on a 24/7 schedule with fixed
check-in and check-out, four collections, per-stay extras, checkout questions
and sample stays, so the template matches the demo out of the box.
### Fixed check-in and check-out are now enforced
Products with **custom duration** and **fixed times** (`fixedTimes`,
`fixedStartTime`, `fixedEndTime`) now have those times enforced by the server.
A customer booking must start at `fixedStartTime` and end at `fixedEndTime` in
the product's schedule timezone (the store timezone if the product has no
schedule), or the API returns `400`. This applies to creating an appointment
(`POST /storefront/appointments`, with or without a cart) and to customer
reschedules (`PUT /storefront/appointments/{appointmentId}/reschedule`). Bookings and reschedules made by the
merchant in the dashboard are exempt, so you can still give a guest an early
check-in or a late check-out.
A fixed-times stay is priced by whole nights: a 15:00 → 11:00 stay of N nights
is shorter than N × 24 hours and is charged as N nights. No SDK changes. If your
site already sends check-in → check-out slots built from the product's fixed
times, nothing changes for you.
[Live demo ↗](https://template-bali-villas.vercel.app) ·
[GitHub ↗](https://github.com/letsopencals/template-bali-villas) ·
[Template docs](/docs/open-source/bali-villas)
# Checkout: existing customers and booking addresses
Source: https://opencals.com/docs/changelog/checkout-customer-trust
Storefront checkout no longer returns tokens for, or changes, an existing customer identified only by the email in the request. Addresses entered for such a booking are saved to the customer once the order is placed.
## Existing customers at checkout
Storefront API keys are public, so an email or `customerId` in a checkout
request is a claim, not proof of who is booking. A customer named in the
request is now only treated as the caller when the request is signed in as
that customer (customer `Authorization` token), or when this cart's checkout
created them.
When a guest enters the email of an existing customer and isn't signed in:
* The booking is still attached to that customer, so it shows in their account
after they sign in (for example with a login code).
* Nothing on their profile is overwritten: name, phone, billing address and
marketing consent stay as they were.
* `checkout/submit` returns no `auth` or `customer`, and `checkout/save-customer`
returns `{}` without a `customerId`.
* Cart and appointment responses show `customer` and `customerId` as `null`.
The `customerFirstName`, `customerEmail` and other snapshot fields echo what
was entered.
### Addresses
`checkout/save-customer` and `checkout/start` don't write to the customer.
Until the order is placed, billing and delivery addresses entered for such a
booking belong to the cart only, and their `customerId` is `null` (for example
`cart.billingAddress.customerId`). When `checkout/submit` places the order,
they're added to the customer's saved addresses. The order and invoice use
them as entered. Treat an address's `customerId` as nullable.
### Billing at submit
Required billing fields are checked against the `billingAddress` in the
request, or, when the request has none, against the billing address already
saved on the cart. You no longer need to send the billing address again at
`checkout/start` or `checkout/submit`. A billing address sent for the first
time with `checkout/submit` is linked to the order. A submit rejected for missing customer
details leaves the cart's reservation in place until the cart expires as
usual.
**What to change in your integration:** always treat `auth` on the submit
response as optional, and don't rely on `save-customer` returning a
`customerId`. When it doesn't, either send the same customer details again in
the next steps or leave `customer` out: the cart already holds the customer.
# Clear Care — clinic template
Source: https://opencals.com/docs/changelog/clarity-template
Clear Care, a free open-source medical clinic booking website template built on the Storefront API — Next.js 15, department-first booking, medical intake, passwordless accounts, Stripe checkout.
## Clear Care — open-source clinic booking template
The third open-source Opencals template is live. **Clear Care** is a medical
clinic booking website — the clinical counterpart to
[Haar](/docs/changelog/haar-template) and [Frisor](/docs/changelog/frisor-template): the
same [Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) underneath, a different vertical and a
warm, organic clinical design.
Every screen — departments, service picker, clinician and location selectors,
time slots, medical intake, checkout, customer account — is built with the
public API.
* Department-first browsing (product collections) across nine specialties
* Clinician & multi-location booking, real-time availability with live group capacity
* Medical intake captured at checkout and saved to the appointment
* Passwordless customer accounts — 6-digit email login with password fallback, one-time email links via `/link/[token]`
* Stripe payments with deposit support
* Stack: Next.js 15, TypeScript, Tailwind v4, `@opencals/storefront-sdk`
* MIT licensed — clone, rebrand, deploy. One-click Vercel deploy.
[Live demo ↗](https://template-clarity.vercel.app) ·
[GitHub ↗](https://github.com/letsopencals/template-clarity) ·
[Template docs](/docs/open-source/clarity)
# Custom Attributes
Source: https://opencals.com/docs/changelog/custom-attributes
Attach arbitrary key-value data to appointments at booking time. Storefront SDK v0.3.11.
## Custom attributes on appointments
Custom booking frontends can now attach arbitrary data to an appointment at
creation time — a booking source, a party size, an external reference id, or
small JSON strings. Custom attributes are a flat map of string keys to
string values (the same idea as Shopify order note/custom attributes): no data
types, no access rules, not searchable — just a simple bag of key-value pairs
that travels with the appointment.
**What's in the release:**
* `POST /storefront/appointments` accepts an optional `customAttributes` object —
a map of string keys to string values.
* Appointment responses now echo `customAttributes`.
* The merchant dashboard shows custom attributes on the appointment detail page.
**Key behaviors:**
* Keys and values are always strings (a value can itself be a JSON string — it is
stored verbatim and never parsed).
* Limits: up to **250** attributes per appointment, keys up to **255** characters,
values up to **5000** characters.
* Not searchable by design — attributes are stored inline with the appointment for
fast reads, not indexed for querying.
**SDK support** — available in `@opencals/storefront-sdk`
[v0.3.11](/docs/changelog/storefront-sdk) and higher. `CreateAppointment` gains an
optional `customAttributes` field.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
# Create an appointment with custom attributes
curl -X POST -H "X-Api-Key: sfk_your_key" \
-H "Content-Type: application/json" \
-d '{
"slot": { "fromDate": "2026-08-15", "fromTime": "10:00:00", "toDate": "2026-08-15", "toTime": "11:00:00", "productId": "..." },
"customAttributes": { "source": "widget", "party_size": "4" }
}' \
https://api.opencals.com/storefront/appointments
```
# Developer docs launched
Source: https://opencals.com/docs/changelog/developer-docs
The Opencals developer documentation is live — interactive API reference, quickstart, guides, and open-source templates.
## Developer docs launched
The Opencals developer documentation is now live at
**[opencals.com/docs](https://opencals.com/docs)**.
* Interactive [API Reference](/docs/api-reference/introduction) with an in-browser playground — try every endpoint without leaving the docs
* [Quickstart guide](/docs/quickstart) — first API call in under 5 minutes
* [Build a booking page](/docs/guides/build-a-booking-page) — end-to-end tutorial (services → staff → availability → cart → checkout)
* [TypeScript SDK guide](/docs/guides/storefront-sdk) — typed client with Zod schemas and formatting helpers
* [Webhooks overview](/docs/guides/webhooks) — planned event catalog for appointment and order lifecycle events
* [Open-source templates](/docs/open-source/templates) — production-ready booking site starters
# Discounts
Source: https://opencals.com/docs/changelog/discounts
Automatic and promo-code discounts — target by product, staff, location, and schedule, with stacking and usage caps. Storefront SDK v0.3.2.
## Discounts — new feature
Storefronts can now apply **automatic discounts** and **promo codes** to a cart
before checkout. Automatic discounts apply to every qualifying booking without
customer action; code discounts require the customer to enter a promo code.
**What's in the release:**
* `POST /storefront/cart/discount-code` — apply a promo code to the cart
* `DELETE /storefront/cart/discount-code` — remove the applied code
* Every cart-returning endpoint now includes an `appliedDiscounts[]` field and
per-line-item `discounts[]`, so you can render a "you saved \$X" summary
* Order line items carry `OrderLineItemDiscount` records for accurate receipts
and refunds
**Key behaviors:**
* Two methods: **automatic** (applies silently) and **code** (customer enters a
promo code).
* Two value formats: **percentage** or **fixed amount**.
* Two scopes: **line-item** (distributed across qualifying lines) and **order**
(applied to the cart total).
* Targeting by **product**, **staff member**, **location**, and an
**include add-ons** toggle. Discounts re-evaluate on every cart mutation.
* **Scheduling**: always-active, weekly-recurring windows, or fixed date
ranges — evaluated in the store's timezone.
* **Requirements**: minimum purchase amount and/or appointment duration range.
* **Stacking**: combinable discounts stack; exclusive discounts apply the single
best deal for the customer.
* **Usage caps**: total redemption cap and per-customer cap. Usage is tracked
per store on multi-store and Shopify setups.
**SDK support** — available in `@opencals/storefront-sdk`
[v0.3.2](/docs/changelog/storefront-sdk) and higher:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { CartService } from "@opencals/storefront-sdk";
// Apply a promo code
const { data: cart } = await CartService.applyDiscountCode({
body: { code: "WELCOME10" },
});
// cart.appliedDiscounts now reflects the discount
// Remove it
await CartService.removeDiscountCode();
```
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
# Apply a promo code over HTTP
curl -X POST -H "X-Api-Key: sfk_your_key" \
-H "X-Cart-Id: {cartId}" \
-H "Content-Type: application/json" \
-d '{"code": "WELCOME10"}' \
https://api.opencals.com/storefront/cart/discount-code
```
See the [Discounts feature page](https://opencals.com/features/discounts) for
the full overview, and the
[Build a booking page guide](/docs/guides/build-a-booking-page) for where promo codes
fit in the checkout flow.
# Fluently — language school template
Source: https://opencals.com/docs/changelog/fluently-template
Fluently, a free open-source language school website template built on the Storefront API, plus a language-school seed dataset with online classroom links.
## Fluently — open-source language school template
The sixth open-source Opencals template is live, and the first one built for
**schools and courses**. **Fluently** is a language-school website set in New
York with two campuses and an online classroom: the same
[Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) as the other templates, with a
friendly, colorful design.
* **Subjects are collections** and **levels are variants**: group classes with A1–B1 levels, private lessons by length, and a \$0 free trial
* **Format from the data**: online vs campus groups come from location types, so the template works for any school without code changes
* **This week timetable**: upcoming group sessions across every class with live seats left and one-click booking
* **Teachers** from staff, with "Book with " deep links that preselect the teacher
* **Join lesson** button on online bookings, using the online location's `link`
* A level-finder quiz, placement questions at checkout, and extras such as a workbook and lesson recording
* Passwordless student accounts, Stripe checkout and the no-payment path for the trial
* `LanguageSchool` and `FAQPage` structured data, Open Graph, `robots.txt` and `sitemap.xml` configured
* Stack: Next.js 15, TypeScript, Tailwind v4, Framer Motion, `@opencals/storefront-sdk` v0.3.14
* MIT licensed — clone, rebrand, deploy. One-click Vercel deploy.
### Language-school seed dataset
New Dev Stores can choose the **Fluently** dataset (`language_school`). It
seeds six languages, campus and online group classes with level variants,
private lessons, the trial and conversation club, teachers, two campuses and an
online classroom with its meeting link, so the template matches out of the box.
Seeded online locations can now carry a meeting link. No API or SDK changes.
[GitHub ↗](https://github.com/letsopencals/template-fluently) ·
[Template docs](/docs/open-source/fluently)
# Frisor — barbershop template
Source: https://opencals.com/docs/changelog/frisor-template
Frisor, a free open-source barbershop booking website template built on the Storefront API — Next.js 15, Stripe checkout, add-ons, customer accounts.
## Frisor — open-source barbershop booking template
The second open-source Opencals template is live. **Frisor** is the barbershop
counterpart to [Haar](/docs/changelog/haar-template): same
[Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) underneath, a different vertical and a
dark editorial design.
Every screen — services, barber picker, time slots, add-ons, cart, checkout,
customer account — is built with the public API.
* Service catalog, barber & multi-location booking, real-time availability
* Add-ons at checkout, Stripe payments with deposit support
* Customer accounts with reschedule and cancellation
* Stack: Next.js 15, TypeScript, Tailwind v4, `@opencals/storefront-sdk`
* MIT licensed — clone, rebrand, deploy. One-click Vercel deploy.
[Live demo ↗](https://template-frisor-sage.vercel.app) ·
[GitHub ↗](https://github.com/letsopencals/template-frisor) ·
[Template docs](/docs/open-source/frisor)
# Haar — salon template
Source: https://opencals.com/docs/changelog/haar-template
Haar, the first open-source Opencals template — a complete beauty-salon booking website built on the Storefront API.
## Haar — open-source salon booking template
The first open-source Opencals template is live.
**Haar** is a complete beauty-salon booking website built on the
[Storefront API](/docs/api-reference/introduction): service catalog, staff and
multi-location booking, real-time availability, Stripe checkout, and customer
accounts.
* Stack: Next.js 15, TypeScript, Tailwind, `@opencals/storefront-sdk`
* MIT licensed — clone, rebrand, deploy for yourself or clients
* One-click Vercel deploy
[Live demo ↗](https://template-haar.vercel.app) ·
[GitHub ↗](https://github.com/letsopencals/template-haar) ·
[Template docs](/docs/open-source/haar)
# NOIR Drive — car rental template
Source: https://opencals.com/docs/changelog/noir-template
NOIR Drive, a free open-source luxury car rental website template built on the Storefront API, plus a car-rental seed dataset and correct validation for multi-day bookings longer than 24 hours.
## NOIR Drive — open-source luxury car rental template
The fifth open-source Opencals template is live, and the first one built around
**multi-day bookings**. **NOIR Drive** is a luxury car-rental website set in
Dubai: the same [Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) as
[Haar](/docs/changelog/haar-template), [Frisor](/docs/changelog/frisor-template),
[Clear Care](/docs/changelog/clarity-template) and [VOLT](/docs/changelog/volt-template),
with a black cinematic design and cars booked by the day.
Two booking surfaces over one store: a multi-day rental flow for the fleet, and
a classic staff flow for chauffeur packages. Every screen is built with the
public API.
* **Booked by the day** — each car is one staffless product with a 1-day base duration and `allowCustomDuration`, on a continuous 24/7 schedule
* **Range calendar on live availability** — reads `getCurrentAvailabilitiesMerged` and checks on the client that pick-up → return fits inside one range
* **Delivery or garage** — a `DELIVERY` location with the address captured at checkout, or a `PHYSICAL` garage
* **Handover details as custom attributes** — handover and return windows, flight number, collection address
* **Per-day extras** with `durationMultiplied`, plus one-off extras
* Licence and ID uploads and a deposit-and-terms checkbox as checkout questions
* **Chauffeur packages** with real drivers and a preferred-car request
* Rental-aware customer accounts: dates in the store timezone, day count, change dates (same length) or cancel within policy
* `AutoRental` and `Product` structured data, Open Graph, `robots.txt` and `sitemap.xml` configured
* Stack: Next.js 15, TypeScript, Tailwind v4, Framer Motion, `@opencals/storefront-sdk` v0.3.14
* MIT licensed — clone, rebrand, deploy. One-click Vercel deploy.
### Car-rental seed dataset
New Dev Stores can choose the **NOIR Drive** dataset (category Car rental,
`car_rental`). It seeds twelve cars on a 24/7 schedule, a garage and a delivery
location, per-day and fixed extras, the rental checkout questions, and four
chauffeurs with their packages, so the template matches the demo out of the box.
### Fix: bookings longer than 24 hours
Bookings with a custom duration longer than a day are now validated against
**continuous availability**: the server checks that the whole span sits inside
one free window. Before, validation used per-day slot generation and could
reject a valid multi-day booking on a 24/7 schedule. No API or SDK changes.
[Live demo ↗](https://template-noir.vercel.app) ·
[GitHub ↗](https://github.com/letsopencals/template-noir) ·
[Template docs](/docs/open-source/noir)
# Storefront API — v1
Source: https://opencals.com/docs/changelog/storefront-api
The Storefront API is publicly available — the full customer-facing booking flow over REST.
## Storefront API — v1
The Storefront API is now publicly available. It covers the full
customer-facing booking flow:
| Domain | Key endpoints |
| - | - |
| **Products** | List services, collections, variants |
| **Staff members** | List staff, per-staff availability |
| **Locations** | Multi-location support |
| **Appointments** | Real-time slot availability |
| **Cart** | Create and manage a booking cart |
| **Checkout** | Initiate and confirm checkout |
| **Payments** | Stripe-backed deposits and full payments |
| **Customer self-service** | View, reschedule, and cancel bookings |
| **Webhooks** | Signed event delivery *(coming soon)* |
Base URL: `https://api.opencals.com` · Authentication: `Authorization: Bearer sfk_…`
The API is versioned via the `X-Api-Version` header. See the
[full endpoint catalog](/docs/api-reference/introduction).
# Storefront SDK
Source: https://opencals.com/docs/changelog/storefront-sdk
Version history for @opencals/storefront-sdk — the official TypeScript client for the Storefront API. See which release each feature first shipped in.
`@opencals/storefront-sdk` is the official TypeScript client for the Storefront
API — generated from the OpenAPI spec, fully typed, with Zod runtime validation
and formatting helpers for prices, durations, and timezones. MIT licensed.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm install @opencals/storefront-sdk
```
[npm ↗](https://www.npmjs.com/package/@opencals/storefront-sdk) ·
[SDK guide](/docs/guides/storefront-sdk)
Each feature ships in a specific SDK version. To use a feature, install that
version or higher — for example, [add-ons](/docs/changelog/add-ons) require
**v0.3.0+** and [discounts](/docs/changelog/discounts) require **v0.3.2+**. Older
versions don't include those methods or types.
## Version history
**Staff contact details removed.** The Storefront API no longer returns staff
`email`, `userId` or `refreshToken`, and staff detail no longer includes the
linked `user`. Storefront keys ship in browsers, so these were readable by
anyone. The fields are gone from every `*StaffMember` type, and the
`StaffMemberDetailUser` type is removed. If you used `email` for avatar
initials, use `firstName` / `lastName` instead.
**Appointment `externalOrderId` / `externalOrderName` deprecated.** These fields
predate orders. Read the platform order from `appointment.order` instead
(`order.externalId`, `order.externalName`).
* Both fields stay in appointment responses and are now filled from the linked
order, so they're correct again for new bookings. They're marked `@deprecated`
in the types.
* `AppointmentService.findByExternalOrderName` is deprecated. It now only
returns appointments from the store making the request, and it returns 404
instead of 400 when nothing matches.
**Custom attributes on appointments.** Ships support for the new
[custom attributes](/docs/changelog/custom-attributes) feature.
* `CreateAppointment` accepts an optional `customAttributes` map (string keys → string values) to attach arbitrary data at booking time
* Appointment responses now echo `customAttributes`
**Discounts & passwordless auth.** Ships support for the new
[discounts](/docs/changelog/discounts) feature and magic-link sign-in.
* `CartService.applyDiscountCode()` and `CartService.removeDiscountCode()`
* New discount types: `AppliedDiscount`, `ApplyCode`, `CartItemAppliedDiscount`, `OrderLineItemDiscount`, `OrderLineItemDiscountWritable`
* Passwordless / magic-link auth: `AuthService.requestLoginCode()`, `AuthService.verifyLoginCode()`, `AuthService.resolveLink()`, with `RequestLoginCode`, `VerifyLoginCode`, `ResolveLinkResponse` types
* Fixed the `AppointmentAddOn` type, which was missing fields due to a backend OpenAPI schema name collision (the DTO was renamed to `CreateAppointmentAddOnDto`)
**Add-ons support.** First version with the [Add-Ons](/docs/changelog/add-ons)
feature.
* `AddonService`, with `CartAddOnItem`, `AppointmentAddOn`, and `OrderAddOnLineItem` types
* `StoreService.getStorePublicSettings()` for storefront currency and public store settings
**Service classes.** Released alongside the public
[Storefront API v1](/docs/changelog/storefront-api).
* Grouped SDK methods into `*Service` classes by tag (e.g. `ProductService`, `AuthService`, `CartService`)
* Stripped the controller prefix from operation IDs (e.g. `ProductService.list()` instead of `productList()`)
* Fixed `Auth` operations missing from the spec (tag collision with the admin controller)
* Rewrote the README with updated examples and a service table
Initial releases of the auto-generated TypeScript client.
* **v0.1.0** — auto-generated client, Zod validation schemas, `setupOpencals()` one-line init, API-key/error/logging interceptors, and helpers (`formatPrice`, `formatDuration`, timezone conversion). Full support for products, cart, checkout, customer auth, appointments, orders, and feedback.
* **v0.1.1** — renamed `external_domain` → `externalDomain` on `StorePublicSettings`.
* **v0.1.2** — renamed `client_secret` → `clientSecret` on `CheckoutStartResponse`.
# VOLT — padel & squash template
Source: https://opencals.com/docs/changelog/volt-template
VOLT, a free open-source padel and squash club booking website template built on the Storefront API — Next.js 15, a multi-court booking grid with custom durations, coach-led training, Stripe checkout, passwordless accounts.
## VOLT — open-source padel & squash club booking template
The fourth open-source Opencals template is live, and the first one built
around **staffless resources** instead of staff-led appointments. **VOLT** is a
padel & squash club booking website: the same
[Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) as
[Haar](/docs/changelog/haar-template), [Frisor](/docs/changelog/frisor-template) and
[Clear Care](/docs/changelog/clarity-template), with a dark technical design and a
different booking shape.
Two booking surfaces over one store: an in-page court grid for rentals, and a
classic coach-led flow for lessons and group classes. Every screen is built
with the public API.
* **Multi-court booking grid** — every court and every open slot for a day in one view, with a padel/squash toggle
* **Custom durations** — 30-minute steps to two hours, composed from consecutive free slots and priced live
* **Staffless resources** — courts use the `Self` rule with `maxAttendees: 1`, so each court is an independent, exclusively-booked unit
* **Coach-led training** — filter by sport, level and format; "Any coach — next available" leads the picker; group classes show live "N left" capacity
* Add-ons and custom checkout questions, Stripe payments with deposit support
* Passwordless customer accounts — 6-digit email login with password fallback, auto-login after checkout, one-time email links via `/link/[token]`
* `SportsActivityLocation` structured data, Open Graph, `robots.txt` and `sitemap.xml` configured
* Stack: Next.js 15, TypeScript, Tailwind v4, Framer Motion, `@opencals/storefront-sdk` v0.3.14
* MIT licensed — clone, rebrand, deploy. One-click Vercel deploy.
If you are booking courts, bays, lanes, studios or rooms rather than people,
VOLT is the closest starting point of the four.
[Live demo ↗](https://template-volt.vercel.app) ·
[GitHub ↗](https://github.com/letsopencals/template-volt) ·
[Template docs](/docs/open-source/volt)
# Build a booking page
Source: https://opencals.com/docs/guides/build-a-booking-page
Step-by-step tutorial: use the Opencals Storefront API with Next.js and the TypeScript SDK to list services, pick a time slot, manage a cart, and complete checkout.
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](https://www.npmjs.com/package/@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:
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm install @opencals/storefront-sdk
```
```ts lib/opencals.ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { setupOpencals } from "@opencals/storefront-sdk";
setupOpencals({
apiKey: process.env.OPENCALS_API_KEY, // sfk_… from Settings → API Keys
});
```
The API key must stay server-side, so the calls below belong in server components, route handlers, or server actions — not client components.
`ProductService.list()` wraps `GET /storefront/products` and returns a paginated list of active services:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import "@/lib/opencals";
import { ProductService } from "@opencals/storefront-sdk";
const { data } = await ProductService.list({ query: { take: 50 } });
// data.data → Product[], data.meta → pagination info
for (const product of data!.data) {
console.log(product.title, product.price, product.duration);
}
```
A few product fields drive your UI:
| Field | What it means for your booking page |
| - | - |
| `duration` | Service length in seconds |
| `price` | Price (0 = free); `minPrice`/`maxPrice` for variable pricing |
| `maxAttendees` | Greater than 1 → show an attendee count selector |
| `isCustomDuration` | `true` → show a date-range picker instead of single slots |
| `staffMembers` | If populated, let the customer pick a staff member |
| `locations` | If populated, let the customer pick a location |
If customers choose who they book with, `StaffMemberService.list()` wraps `GET /storefront/staff-members`:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { StaffMemberService } from "@opencals/storefront-sdk";
const { data: staff } = await StaffMemberService.list();
```
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.
`ProductService.getCurrentAvailabilities()` wraps `GET /storefront/products/{productId}/current-availabilities`. It takes a required `date` (`YYYY-MM-DD`) plus optional `staffMemberId`, `locationId`, and `timezone` filters:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { ProductService } from "@opencals/storefront-sdk";
const { data: slots } = await ProductService.getCurrentAvailabilities({
path: { productId: product.id },
query: {
date: "2026-06-15",
staffMemberId: selectedStaff?.id,
timezone: "America/New_York",
},
});
```
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.
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:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { AppointmentService } from "@opencals/storefront-sdk";
const { data: appointment } = await AppointmentService.create({
body: {
slot: {
productId: product.id,
fromDate: "2026-06-15",
fromTime: "14:00:00",
toDate: "2026-06-15",
toTime: "15:00:00",
staffMemberId: selectedStaff?.id ?? null,
locationId: null,
},
numberOfAttendees: 1,
},
});
```
All slot date/times are **UTC** — convert from the customer's timezone before sending.
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:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { CartService } from "@opencals/storefront-sdk";
// Create a cart (or fetch the existing one if you already have an ID)
const { data: cart } = await CartService.createOrGet({
headers: { "X-Cart-Id": existingCartId ?? "" },
});
// Add the appointment to it
const { data: updatedCart } = await CartService.addItem({
body: { appointmentId: appointment!.id },
headers: { "X-Cart-Id": cart!.id },
});
```
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.
If the service has add-ons (extras like equipment rentals, upgrades, or accessories), fetch them and let the customer choose before proceeding to payment.
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { AddOnService, CartService } from "@opencals/storefront-sdk";
// Fetch add-ons available for the booked service
const { data: addOns } = await AddOnService.listForProduct({
path: { productId: product.id },
});
// addOns.data → AddOn[] with title, description, price, durationMultiplied, maxQuantity
```
When the customer selects an add-on, attach it to the cart item:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
await CartService.addAddOn({
body: {
cartItemId: updatedCart!.items[0].id,
addOnId: selectedAddOn.id,
quantity: 1,
},
headers: { "X-Cart-Id": cart!.id },
});
```
Add-on pricing fields:
| Field | What it means |
| - | - |
| `price` | Unit price. Multiply by quantity for the line total. |
| `durationMultiplied` | If `true`, quantity is auto-set to the service duration units (e.g., 3 days = quantity 3). Don't ask the customer for quantity. |
| `maxQuantity` | If non-null, cap your quantity input at this value. |
Skip this step if `addOns.data` is empty — not every service has add-ons configured.
If the store offers discount codes, let customers enter a promo code before checkout. `CartService.applyDiscountCode()` wraps `POST /storefront/cart/discount-code`:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { CartService } from "@opencals/storefront-sdk";
try {
const { data: updatedCart } = await CartService.applyDiscountCode({
body: { code: "SUMMER15" },
headers: { "X-Cart-Id": cart!.id },
});
// updatedCart now includes applied discounts
} catch (error) {
// Handle typed errors: CODE_NOT_FOUND | CODE_EXPIRED | CODE_DISABLED | CODE_LIMIT_REACHED | CODE_CUSTOMER_INELIGIBLE | CODE_NO_MATCHING_ITEMS
// Render inline below the input (Shopify-style red)
}
```
Remove a code with `DELETE /storefront/cart/discount-code`:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
await CartService.removeDiscountCode({
headers: { "X-Cart-Id": cart!.id },
});
```
### 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:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const originalTotal = updatedCart!.items.reduce(
(sum, item) => sum + item.originalPrice * item.quantity,
0
);
const saved = originalTotal - updatedCart!.total;
```
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`):
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { CheckoutService } from "@opencals/storefront-sdk";
const { data: checkout } = await CheckoutService.start({
body: { provider: "stripe" },
headers: { "X-Cart-Id": cart!.id },
});
// Stripe: checkout.clientSecret → confirm with Stripe Payment Element
// Redirect providers: checkout.redirectUrl → send the customer there
```
**Save the customer** so the order and appointments have contact details (guests don't need an account):
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
await CheckoutService.saveCustomer({
body: {
customer: {
email: "jane@example.com",
firstName: "Jane",
lastName: "Doe",
},
},
headers: { "X-Cart-Id": cart!.id },
});
```
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:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const { data: result } = await CheckoutService.submit({
body: { stripePaymentIntentId: paymentIntentId }, // if confirmed client-side with Stripe
headers: { "X-Cart-Id": cart!.id },
});
// result.order → the confirmed order
// result.auth → tokens when this checkout created the customer or the request is signed in as them
```
The response includes the created `order` and, for new customers, an `auth` token pair you can use to sign them in automatically — see [Authentication](/docs/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](https://opencals.com/docs/open-source/templates) or clone the [Haar salon template](https://github.com/letsopencals/template-haar). For the full endpoint catalog, head to the [API reference](/docs/api-reference/introduction).
# Customer email links (magic links)
Source: https://opencals.com/docs/guides/customer-email-links
Set up passwordless email links for customer actions: view/reschedule/cancel appointments, leave feedback, verify email, and reset passwords. No additional code — just configure one setting and implement one route.
Customer email links let your customers interact with their bookings directly from email. When Opencals sends an appointment confirmation, feedback request, or password reset, the link in the email takes them directly to the right page — signed in automatically.
This guide is for merchants building custom frontends with the Opencals Storefront SDK. If you use the multi-tenant Opencals storefront (salon.opencals.com), no setup is needed.
## Overview
All customer email links follow the same pattern:
* Every link is `{storefrontBaseUrl}/link/{token}`
* Your app implements one route: `GET /link/{token}`
* The route calls `Auth.resolveLink(token)` to get the customer's action and redirect target
* You sign the customer in (if tokens are provided) and redirect them
## Setup
### Step 1: Set the `storefrontBaseUrl` Setting
In the Opencals dashboard, go to **Settings → API → Storefront Base URL** and enter your app's base URL:
```
https://mybooking.example.com
```
This tells Opencals where to send customer email links. If you don't set it, Opencals will fall back to the store's domain, and links may not reach your custom frontend.
### Step 2: Implement the `/link/{token}` Route
Your app must handle `GET /link/{token}`. Here's an example with Next.js:
```ts app/link/[token]/page.tsx theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { redirect } from "next/navigation";
import { setupOpencals, AuthService } from "@opencals/storefront-sdk";
// Ensure SDK is initialized (your server config should call setupOpencals once at startup)
setupOpencals();
export default async function MagicLinkPage({ params }: { params: { token: string } }) {
try {
// Resolve the token: validates it, signs the customer in, and returns redirect info
const { data: response } = await AuthService.resolveLink({
query: { token: params.token }
});
if (!response) {
return Invalid or expired link
;
}
const { redirectPath, accessToken, refreshToken, requiresAction } = response;
// If tokens are provided, save them for future requests
if (accessToken && refreshToken) {
// Option 1: Store in an httpOnly cookie (recommended)
// You can set this via a server action or route handler
// Option 2: Redirect to a handler that stores tokens and redirects
// This avoids passing tokens in the URL
}
// Redirect the customer to their action
redirect(redirectPath || "/account");
} catch (error) {
return This link has expired or is invalid
;
}
}
```
For production, store the tokens securely (httpOnly cookies) and redirect via a handler:
```ts app/api/auth/link/route.ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { cookies } from "next/headers";
import { setupOpencals, AuthService } from "@opencals/storefront-sdk";
setupOpencals();
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const token = searchParams.get("token");
if (!token) {
return new Response("Missing token", { status: 400 });
}
try {
const { data: response } = await AuthService.resolveLink({
query: { token }
});
if (response?.accessToken) {
const cookieStore = await cookies();
cookieStore.set("accessToken", response.accessToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 3600, // 1 hour
});
if (response.refreshToken) {
cookieStore.set("refreshToken", response.refreshToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 604800, // 7 days
});
}
}
return Response.redirect(response?.redirectPath || "/account");
} catch (error) {
console.error("Link resolution failed:", error);
return new Response("Invalid or expired link", { status: 400 });
}
}
```
## Purpose → Redirect Table
This table shows what each link type does and where the customer is redirected:
| Purpose | Customer Receives | Redirect Path | Tokens Provided? |
| - | - | - | - |
| `view-appointment` | Link from confirmation email | `/account/appointments/{id}/view` | Yes (auto sign-in) |
| `cancel-appointment` | Link to cancel or reschedule | `/account/appointments/{id}/view?action=cancel` | Yes (auto sign-in) |
| `reschedule-appointment` | Link to reschedule | `/account/appointments/{id}/view?action=reschedule` | Yes (auto sign-in) |
| `leave-feedback` | Link to leave a review | `/account/appointments/{id}/view?action=feedback` | Yes (auto sign-in) |
| `verify-email` | Link to confirm email address | `/account` | Yes (auto sign-in) |
| `reset-password` | Link to reset forgotten password | `/auth/reset-password?token={token}` | No (tokens passed separately) |
## Special Case: Password Reset
Password reset links do **not** return customer tokens. Instead, the token is passed as a query parameter so your app can verify it when the customer submits a new password.
### Example: Password Reset Handler
```ts app/auth/reset-password/page.tsx theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
"use client";
import { useState } from "react";
import { useSearchParams } from "next/navigation";
import { AuthService } from "@opencals/storefront-sdk";
export default function ResetPasswordPage() {
const searchParams = useSearchParams();
const token = searchParams.get("token");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
if (!token) {
return Invalid reset link
;
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
// Call the API with the token from the URL
await AuthService.resetPassword({
body: { token, newPassword: password }
});
// Password reset successful — redirect to sign-in
window.location.href = "/auth/sign-in?success=Password%20reset%20successful";
} catch (err: any) {
setError(err.message || "Failed to reset password");
} finally {
setLoading(false);
}
};
return (
);
}
```
## Passwordless Email Login (Optional)
In addition to email links, you can offer passwordless login with 6-digit codes:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
// 1. Customer requests a code
await AuthService.requestLoginCode({
body: { email: "customer@example.com" }
});
// Email is sent with 6-digit code
// 2. Customer enters the code
const { data: tokens } = await AuthService.verifyLoginCode({
body: { email: "customer@example.com", code: "123456" }
});
// 3. Tokens are returned — sign them in
```
The code expires in 5 minutes and allows 5 failed attempts before a new code must be requested.
## SDK Methods
The following SDK methods are used for email links and passwordless auth:
| Method | Purpose |
| - | - |
| `AuthService.resolveLink({ query: { token } })` | Validate and resolve a magic link token |
| `AuthService.resetPassword({ body: { token, newPassword } })` | Reset password with token |
| `AuthService.requestLoginCode({ body: { email } })` | Send 6-digit code to email |
| `AuthService.verifyLoginCode({ body: { email, code } })` | Verify code and get tokens |
See the [API reference](/docs/api-reference/introduction) for full request and response schemas.
## Security Checklist
* Store API keys in environment variables — never in client code.
* Call `resolveLink` on the server, not the client.
* Store customer tokens in **httpOnly cookies** (not localStorage) when possible.
* Redirect after storing tokens to avoid exposing them in the URL.
* For password reset, store the token as a query parameter only (it does not grant authentication).
* Tokens expire according to their purpose (1 hour for verify-email/reset-password, 7 days for leave-feedback, 24 hours default).
## Troubleshooting
**"Invalid or expired link"**
* The token may have been used already (one-time use only).
* The token may have expired (check the purpose expiry in the earlier table).
* The store context may not match the API key's store.
**Customer not signed in after clicking the link**
* Verify tokens were stored (httpOnly cookies, session, etc.).
* Check that `AuthService.resolveLink` returned `accessToken` and `refreshToken`.
* For `reset-password` links, no sign-in occurs — the customer must reset their password and sign in manually.
**Password reset token not working**
* The token is included in the URL query string only, not in the API call.
* Pass it to `AuthService.resetPassword({ body: { token, newPassword } })` exactly as received.
# Storefront SDK
Source: https://opencals.com/docs/guides/storefront-sdk
Install and use @opencals/storefront-sdk — the type-safe TypeScript client for the Opencals booking API, generated from the OpenAPI spec with Zod schemas and helpers included.
[@opencals/storefront-sdk](https://www.npmjs.com/package/@opencals/storefront-sdk) is the official type-safe TypeScript client for the Storefront API. It's generated from the same OpenAPI specification as the [API reference](/docs/api-reference/introduction), so every request and response is fully typed, and it ships with Zod schemas for runtime validation plus formatting helpers for prices, durations, and timezones. MIT licensed.
## Installation
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm install @opencals/storefront-sdk
```
## Setup
Call `setupOpencals()` once at app startup — a root layout, an API client singleton, or a module you import from your server code:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { setupOpencals } from "@opencals/storefront-sdk";
setupOpencals({
apiKey: "sfk_…", // or set the OPENCALS_API_KEY env var
baseUrl: "https://…", // optional, defaults to https://api.opencals.com
logging: true, // optional request/response logging
errorHandler: true, // throw OpencalsApiError on failures (default)
customerToken: "eyJ…", // optional customer JWT for authenticated requests
});
```
With the `OPENCALS_API_KEY` environment variable set, `setupOpencals()` needs no arguments.
The API key is attached to every request automatically — which also means SDK calls belong on the server, where the key is safe.
## Services
The SDK mirrors the API's tags as static service classes:
| Service | Methods |
| - | - |
| `ProductService` | `list`, `get`, `getBySlug`, `getByExternalId`, `getCurrentAvailabilities`, `getCurrentAvailabilitiesMerged`, `getNearestAvailability` |
| `AuthService` | `signIn`, `signUp`, `oauth`, `refresh`, `requestPasswordReset`, `resetPassword`, `requestEmailVerification`, `verifyEmail` |
| `CartService` | `get`, `createOrGet`, `addItem`, `removeItem`, `extendExpiration`, `applyDiscountCode`, `removeDiscountCode` |
| `CheckoutService` | `start`, `submit`, `saveCustomer`, `saveAnswers`, `getCartQuestions` |
| `AppointmentService` | `list`, `find`, `create`, `cancel`, `reschedule`, `feedback`, `getBookingPreferences` |
| `OrderService` | `list`, `find` |
| `SelfService` | `getProfile`, `updateProfile`, `changePassword` |
| `LocationService` | `list`, `get`, `getBySlug` |
| `StaffMemberService` | `list`, `getBySlug` |
| `PaymentService` | `getAvailableProviders`, `getSettings` |
| `StoreService` | `getStorePublicSettings` |
| `ProductCollectionService` | `list`, `getBySlug` |
| `ImageService` | `get` |
| `CheckoutQuestionService` | `listTranslations` |
| `FeedbackQuestionService` | `listTranslations` |
Every method takes a single options object with typed `path`, `query`, `body`, and `headers` fields matching the underlying endpoint:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { ProductService } from "@opencals/storefront-sdk";
// GET /storefront/products/slug/{slug}
const { data: product } = await ProductService.getBySlug({
path: { slug: "haircut" },
});
// GET /storefront/products/{productId}/current-availabilities
const { data: slots } = await ProductService.getCurrentAvailabilities({
path: { productId: product!.id },
query: { date: "2026-06-15", timezone: "America/New_York" },
});
```
Cart and checkout calls pass the cart ID through the typed `headers` field:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { CartService } from "@opencals/storefront-sdk";
const { data: cart } = await CartService.createOrGet({
headers: { "X-Cart-Id": cartId ?? "" },
});
await CartService.addItem({
body: { appointmentId },
headers: { "X-Cart-Id": cart!.id },
});
```
## Customer-authenticated requests
For endpoints that act on behalf of a signed-in customer, either pass `customerToken` to `setupOpencals()` or set the header per call:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { AppointmentService } from "@opencals/storefront-sdk";
const { data: appointments } = await AppointmentService.list({
headers: { Authorization: `Bearer ${accessToken}` },
});
```
See [Authentication](/docs/authentication) for how to obtain and refresh customer tokens.
## Error handling
With the default error handler enabled, failed requests throw a typed `OpencalsApiError`:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { OpencalsApiError, ProductService } from "@opencals/storefront-sdk";
try {
await ProductService.get({ path: { productId: "missing" } });
} catch (error) {
if (error instanceof OpencalsApiError) {
console.error(error.message);
}
}
```
## Zod validation
Response types come with auto-generated Zod schemas, useful when data crosses a trust boundary (for example, after passing through your own API routes):
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { zProductListResponse } from "@opencals/storefront-sdk";
const result = zProductListResponse.safeParse(apiResponse);
```
## Helpers
The SDK includes small utilities you'd otherwise rewrite in every booking UI:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import {
formatPrice,
formatDuration,
toLocalFromUtc,
toUtcFromLocal,
mergeTimeIntervals,
} from "@opencals/storefront-sdk";
formatPrice(4500, "USD"); // "$45.00"
formatDuration(3600); // "1h"
toLocalFromUtc("2026-06-15T14:00:00Z", "America/New_York");
```
`toLocalFromUtc` / `toUtcFromLocal` handle the UTC conversion the API expects for appointment slots, and `mergeTimeIntervals` is handy when rendering availability ranges.
## Examples
The [Build a booking page](/docs/guides/build-a-booking-page) tutorial walks through a full implementation, and the open-source [booking templates](https://opencals.com/docs/open-source/templates) are complete Next.js apps you can fork.
# Webhooks
Source: https://opencals.com/docs/guides/webhooks
The planned Opencals webhook event catalog for booking automations — and how to keep a headless booking site in sync today with polling and revalidation.
**Webhooks are not yet available.** This page documents the planned design so you can prepare your integration architecture. Event names and payload shapes may change before release. Until webhooks ship, use the [polling and revalidation patterns](#keeping-data-fresh-today) below — they cover the same use cases reliably.
Webhooks will let Opencals notify your application the moment something happens in your store — a booking is created, an order is paid, a customer cancels — so you can automate without polling.
## Planned events
The first release focuses on the six events that power the most common merchant automations:
| Event | Fires when | Common use cases |
| - | - | - |
| `appointment.created` | A new booking is made | CRM sync, welcome and confirmation flows |
| `order.paid` | Payment is captured for an order | Fulfillment, accounting entries |
| `appointment.canceled` | A booking is canceled | Waitlist backfill, freeing resources |
| `appointment.rescheduled` | A booking is moved to a new slot | Calendar updates, staff notifications |
| `appointment.completed` | A booking's end time passes | Review requests, follow-up sequences |
| `order.refunded` | A refund is issued on an order | Accounting corrections, inventory release |
## How delivery will work
A webhook is an HTTP `POST` from Opencals to an endpoint you host. Each delivery:
* is sent as JSON to the URL you register,
* identifies its event type so one endpoint can handle many events,
* is **signed**, so you can verify the request really came from Opencals,
* expects a `2xx` response. Non-2xx responses are treated as failures and retried with backoff, so make your handler idempotent — the same event may be delivered more than once.
Respond quickly (acknowledge first, process asynchronously) to avoid timeouts on slow handlers.
### Verifying signatures
Deliveries will follow the standard HMAC pattern: a signature header computed over the **raw request body** with your endpoint's secret. The verification you'd write today won't change:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhookSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader);
return a.length === b.length && timingSafeEqual(a, b);
}
```
Verify against the raw, unparsed request body — re-serialized JSON can reorder keys and break the comparison. In Next.js route handlers, use `await request.text()` before parsing.
## Keeping data fresh today
Until webhooks ship, two patterns keep an integration accurate:
* **Poll customer-scoped endpoints on your own schedule.** `GET /storefront/appointments` and `GET /storefront/orders` return current state, so a periodic sync job covers CRM, accounting, and follow-up automations. Respect the [rate limits](/docs/quickstart#rate-limits) — a sync every few minutes is well within them.
* **Time-based revalidation for catalog data.** Products, staff, and locations change rarely — Next.js ISR (`revalidate`) or a short CDN cache of a few minutes is usually enough for a headless booking site.
* **Always fetch availability live.** Time slots change with every booking, so call the availability endpoints at request time rather than caching them. The booking flow is also self-correcting: `POST /storefront/cart/checkout/start` re-validates every cart item and reports invalid ones before any payment is taken.
## Related
* [Build a booking page](/docs/guides/build-a-booking-page) — where cached catalog data and live availability fit in the flow.
* [API reference](/docs/api-reference/introduction) — every endpoint the planned events map to.
# Introduction
Source: https://opencals.com/docs/introduction
The Opencals Storefront API is booking infrastructure for builders — a REST API for services, real-time availability, carts, checkout, and payments, so you can build a fully custom booking experience on any stack.
Opencals is a **standalone-first booking platform**: the dashboard runs your scheduling, staff, services, and payments, and the **Storefront API** exposes all of it over REST so you can build the customer-facing side yourself — your own booking page, your own stack, your own design.
Think of it as booking infrastructure for builders. Opencals handles the scheduling engine, payments, and conflict prevention — you keep full control of the front end.
## What you can build
* A custom booking page on your existing website, in any framework
* A headless booking site generated statically and revalidated on demand
* A customer self-service portal — appointments, rescheduling, cancellations, order history
* Anything else that needs services, time slots, and checkout as an API
If you can configure it in the dashboard, you can read it over the API.
## Start here
Get an API key and make your first request in a few minutes.
The full flow — services, availability, cart, and checkout — with Next.js and the TypeScript SDK.
Every endpoint, parameter, and response shape, with an interactive playground.
The typed TypeScript client, generated from the same OpenAPI spec as these docs.
## Skip the boilerplate
If you'd rather start from working code, Opencals publishes free open-source booking templates built on this exact API — browse them at [opencals.com/docs/open-source/templates](https://opencals.com/docs/open-source/templates).
# Bali Villa Rentals — villa booking template
Source: https://opencals.com/docs/open-source/bali-villas
Free, open-source villa and holiday-rental website template. Next.js 15, nightly stays with fixed check-in and check-out, a calendar on live availability, per-stay extras, guest details, Stripe checkout and passwordless accounts, built on the Opencals Storefront API.
A complete villa-rental website you can clone and deploy today. Bali Villa
Rentals is the first template built around **nightly stays**: each villa is
booked by the night, from a check-in date to a check-out date, with a fixed
**check-in at 15:00 and check-out at 11:00**. Start here if you rent villas,
cabins, guest houses or rooms by the night. It runs on the same
[Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) as [Haar](/docs/open-source/haar),
[Frisor](/docs/open-source/frisor), [Clear Care](/docs/open-source/clarity),
[VOLT](/docs/open-source/volt), [NOIR Drive](/docs/open-source/noir) and
[Fluently](/docs/open-source/fluently).
The demo brand is **Lumbung Sidemen**, ten villas in the rice fields of
Sidemen, East Bali (USD, `Asia/Makassar`). The design is warm and quiet:
limestone paper, jungle green and a single terracotta accent, with every
nightly rate shown.
template-bali-villas.vercel.app
letsopencals/template-bali-villas (MIT)
One-click deploy
rentals-multi-day reference
## Nightly stays
Guests pick a villa, then a check-in and a check-out date on a calendar that
reads the villa's live availability. Booked nights are struck through, and the
calendar stops the check-out date at the next booking. Guests (adults, children,
infants), arrival time, flight number and the occasion are collected in the same
flow. Extras such as an airport transfer or a floating breakfast are added once
per stay. The total is always **nights × nightly rate + extras**, the same maths
the engine charges.
## The stay model
This is the part worth copying even if you never ship this template. It uses
existing Opencals features only.
| Piece | How it's modelled |
| - | - |
| A villa | One staffless product in its own product pool, `duration: 86400` (one night), `allowCustomDuration: true`, `maxDuration` (21 nights in the demo), `maxAttendees: 1` |
| Check-in and check-out | `fixedTimes: true`, `fixedStartTime: "15:00:00"`, `fixedEndTime: "11:00:00"`. The API rejects a customer booking that doesn't start and end at these times |
| Availability across midnight | A continuous 24/7 schedule |
| Date picker | `getCurrentAvailabilitiesMerged` **without** `duration`; the client checks that the stay fits inside one range |
| The slot | Check-in date 15:00 → check-out date 11:00 in the schedule timezone, sent in UTC |
| Turnover day | A check-out day can be the next guest's check-in day (out at 11:00, in at 15:00) |
| Guests, arrival time, flight number, occasion | Appointment [custom attributes](/docs/changelog/custom-attributes) |
| Minimum nights | Per-villa content in the template (`lib/villa-content.ts`), checked in the calendar |
| Extras | Add-ons charged once per stay |
| Cancellation | `allowCustomerCancel` with `cancelGap` of 14 days; customers can also move their dates |
A stay at Lumbung Sidemen, check-in 14 Nov and check-out 17 Nov, becomes
`{ fromDate: '2026-11-14', fromTime: '07:00:00', toDate: '2026-11-17', toTime: '03:00:00' }`
(UTC+8): three nights, priced as three units, three × the nightly rate.
Give each villa its own product rather than a variant: variants don't block each
other, so the same villa could be booked twice.
## What's inside
| Feature | Notes |
| - | - |
| Home | Video hero with villa pins and a date and guest search, the valley, a day at the resort, experiences, testimonials and FAQ |
| Villas | All ten villas with collection filters (rice barns, pool villas, family houses, signature), a search by dates and guests, and a map |
| Villa pages | Gallery, room tour, sleeping arrangements, amenities, house rules, reviews and a cancellation timeline next to the booking calendar |
| Reserve | Stay dates, guests and arrival details, per-stay extras, checkout questions, promo code, a hold countdown and Stripe payment |
| Trip-aware accounts | Check-in → check-out dates in the resort timezone, night count and guest details; customers can change dates (same number of nights) or cancel within policy |
| Passwordless accounts | 6-digit email login, one-time email links via `/link/[token]` |
| Editorial pages | Experiences, journal, about and getting there |
| Motion | Lenis smooth scrolling, a gate curtain on first visit, reveals and parallax; every effect has a reduced-motion fallback |
| SEO | Per-page metadata, Open Graph, `LodgingBusiness`, `VacationRental` and `FAQPage` structured data, `robots.txt`, `sitemap.xml` |
| Stack | Next.js 15, TypeScript, Tailwind v4, Framer Motion, `@opencals/storefront-sdk` v0.3.17 |
## Get started
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
git clone https://github.com/letsopencals/template-bali-villas.git
cd template-bali-villas
npm install
```
Create a store in the [Opencals dashboard](https://opencals.com) and choose
the **Bali Villa Rentals** dataset preset (category Villa resort,
`villa_resort`). It seeds the ten villas on a 24/7 schedule with fixed
check-in and check-out, collections, extras, checkout questions and sample
stays. Then grab a Storefront API key (see [Authentication](/docs/authentication))
and set `OPENCALS_API_KEY` and `AUTH_SECRET` in `.env.local`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm run dev
```
Your villas, rates and availability render immediately.
Push to your own repo and
[deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fletsopencals%2Ftemplate-bali-villas\&env=OPENCALS_API_KEY,AUTH_SECRET\&envDescription=API%20key%20from%20your%20Opencals%20dashboard%20and%20a%20random%20secret%20for%20auth\&project-name=bali-villas\&repository-name=template-bali-villas),
or host anywhere Next.js runs.
Pages are server-rendered, so their API calls carry no browser `Origin`.
Leave **allowed origins** empty on the key you give this template, or those
calls are rejected.
Set **Storefront Base URL** in your dashboard (Settings → API) to your
deployed domain. Every customer link (view, reschedule, cancel, leave
feedback) is built as `{storefrontBaseUrl}/link/{token}`, so if it still
points at localhost those emailed links go nowhere.
## Make it yours
MIT-licensed: rebrand it, restructure it, or use it for client work. Business
settings live in `lib/site-config.ts`: name, currency and timezone, check-in and
check-out times, the longest stay, the cancellation window, arrival times,
occasions and contact details. Per-villa rooms, story, amenities and minimum
nights are in `lib/villa-content.ts`. Every colour and font is a Tailwind v4
`@theme` token in `app/globals.css`:
```css theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
@theme {
--color-paper: #F4EEE4; /* limestone page background */
--color-jungle: #1F3A2E; /* dark sections and footer */
--color-terracotta: #B4502E; /* the only accent */
}
```
Building a different stay or rental site with an AI agent? The
[build-booking-site skill](/docs/ai-agents/build-booking-site-skill) has a
`rentals-multi-day` reference with the full pattern, including nightly stays.
# Clear Care — clinic booking template
Source: https://opencals.com/docs/open-source/clarity
Free, open-source medical clinic booking website template. Next.js 15, a guided Care Finder, a single-page booking composer with pre-visit notes and photo uploads, video visits, Stripe checkout and a family care record — built on the Opencals Storefront API.
A complete medical clinic booking website you can clone and deploy today. Clear
Care is the clinic counterpart to [Haar](/docs/open-source/haar) and
[Frisor](/docs/open-source/frisor): the same
[Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) underneath, with a calm "Warm Practice"
design: cream, honey oak, dusty steel-blue, brass and sage, set in Fraunces and
Figtree. Patients start from what's going on rather than a department name, and
every screen (Care Finder, departments, clinicians, video visits, the booking
composer, checkout, the visit pass and the My Care portal) is built with the
public API.
template-clarity.vercel.app
letsopencals/template-clarity (MIT)
One-click deploy
## What's inside
| Feature | Notes |
| - | - |
| Care Finder | `/care` asks who the visit is for, what's going on and in clinic or by video, then recommends visits with the next free times. Needs and keywords live in `lib/site-config.ts`. Not a diagnosis tool; the emergency notice is on every booking surface |
| Rooms of care | Product Collections endpoints — General Medicine, Cardiology, Dermatology, Pediatrics, Physiotherapy, Dental, Nutrition, Mental Health, Labs & Vaccinations, each with what it treats, its visits, clinicians and FAQs |
| Clinicians | Staff Members endpoints — a directory and a profile per clinician with their bookable visits |
| Video visits | The store's online location — chosen per visit in the booking flow, with a local camera/mic check on `/video-visits` |
| Booking composer | One page: who the visit is for (Me / My child / Someone else), clinic or video, clinician or "first available", day and slots, add-ons, then pre-visit notes. A live appointment card is stamped "Held for you" on Reserve |
| Pre-visit notes | Checkout questions mapped by `internalName` in `lib/intake.ts`, answered per visit (so a family cart keeps each patient's notes), with patient name/DOB when it's not for you and real photo uploads via presigned URLs |
| Multi-location | Locations endpoints — three clinics out of the box, clinicians filtered by clinic |
| Checkout | Cart + Checkout + Payment endpoints — two steps (contact, then payment), themed Stripe Elements or pay at the clinic, promo codes, a cart hold with Extend |
| Visit pass | What to bring, add to calendar, and video join details on the thank-you page |
| My Care | Self-Service endpoints — next visit with a prep checklist and join-video window, recalls due, care team, family filter, visit timeline and records with notes, photos and invoices, rate your care, reschedule/cancel within the store's policy, billing and communication preferences |
| Passwordless sign-in | 6-digit email code first, password second, one-time email links via `/link/[token]` |
| Stack | Next.js 15, TypeScript, Tailwind v4, framer-motion, Lenis, `@opencals/storefront-sdk` ^0.3.17 |
## Get started
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
git clone https://github.com/letsopencals/template-clarity.git
cd template-clarity
npm install
```
Create a store in the [Opencals dashboard](https://opencals.com) — choose the
**Medical / Clear Care Medical** dataset preset to seed the clinic's
departments, services, clinicians and pre-visit questions — then grab a
Storefront API key (see [Authentication](/docs/authentication)) and set it in
`.env.local`. Stores seeded before the "Visit for" and patient questions were
added still work, but book every visit for "Myself" until you reseed.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm run dev
```
Your clinic's real departments, clinicians, and availability render immediately.
Push to your own repo and
[deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fletsopencals%2Ftemplate-clarity),
or host anywhere Next.js runs.
## Make it yours
MIT-licensed: rebrand it, restructure it, or use it as a starting point for
client work. Clinic copy and knowledge (Care Finder needs, department
content, prep lists, recalls, clinician bios) live in `lib/site-config.ts`,
intake mapping in `lib/intake.ts`, and every colour and font is a Tailwind v4
`@theme` token, with the full design contract in `DESIGN.md`. Agencies can run one codebase against multiple stores —
each deployment points at a different API key. To go deeper than theming,
follow the [booking page tutorial](/docs/guides/build-a-booking-page) to see the API
calls behind each screen.
# Fluently — language school template
Source: https://opencals.com/docs/open-source/fluently
Free, open-source language school website template. Next.js 15, group classes with level variants and live seats left, private lessons, a free trial, teachers, campuses plus an online classroom with a Join button, Stripe checkout and passwordless accounts, built on the Opencals Storefront API.
A complete language-school website you can clone and deploy today. Fluently
is built for **schools and courses**: group classes with levels, private 1:1
lessons, a free trial, teachers, campuses and a live **online classroom**. Start
here for a language, music, coding, art or tutoring school. It runs on the same
[Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) as [Haar](/docs/open-source/haar),
[Frisor](/docs/open-source/frisor), [Clear Care](/docs/open-source/clarity),
[VOLT](/docs/open-source/volt) and [NOIR Drive](/docs/open-source/noir).
It's set in New York (USD, `America/New_York`) with two campuses and an online
classroom, teaching Spanish, French, German, Italian, Japanese and Mandarin. The
design is friendly and colorful: chunky sticker buttons, speech-bubble chips and
3D mascots next to real photos.
template-fluently.vercel.app
letsopencals/template-fluently (MIT)
One-click deploy
classes-and-lessons reference
## The school model
This is the part worth copying even if you never ship Fluently itself. It uses
existing Opencals features only, and the template reads everything from the
data, so nothing about languages is hard-coded.
| Piece | How it's modelled |
| - | - |
| A subject (a language) | A collection. A separate `start-here` collection holds the trial and conversation club |
| A group class | A product with `maxAttendees > 1`, assigned to its teachers |
| Levels (A1, A2, B1) | Variants of the group class, each with its own slug and price |
| A private lesson | A 1-seat product whose variants are lengths (45 / 60 min) |
| The free trial | A 1-seat product priced `0`, booked without a card |
| Online vs campus | Derived from the class's location types: all `ONLINE` means an online group |
| Teachers | Staff members; their languages and campuses come from their class and location assignments |
| Seats left | `maxAttendees - attendees` on each availability slot |
| The online classroom | The `ONLINE` location's `link`, returned on the booked appointment's location |
| Colors | Each product's dashboard color picks its swatch |
## What's inside
| Feature | Notes |
| - | - |
| Home | Mascot hero with real teacher portraits, stats counted from your data, language tiles, a format switcher, a level-finder quiz, testimonials and FAQ |
| This week timetable | Next seven days of group sessions across every class, with seats left, language filters and one-click booking (one cached availability call per level) |
| Classes | Catalog filtered by language, format and level, with shareable URLs |
| Language and teacher pages | Per-language classes, private lessons and teachers; teacher profiles with "Book with " |
| Booking | Online or campus, level or length, teacher, extras, placement questions, details, payment. Deep links: `/booking/?date=&staff=&location=` |
| Online lessons | A Join lesson button on online bookings, live from 15 minutes before the start |
| Cart & Stripe checkout | Cart + Checkout + Payment endpoints, with the no-payment path for the free trial |
| Passwordless accounts | 6-digit email login (password fallback), one-time email links via `/link/[token]` |
| SEO | Per-page metadata, Open Graph, `LanguageSchool` and `FAQPage` structured data, `robots.txt`, `sitemap.xml` |
| Stack | Next.js 15, TypeScript, Tailwind v4, Framer Motion, `@opencals/storefront-sdk` v0.3.14 |
## Get started
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
git clone https://github.com/letsopencals/template-fluently.git
cd template-fluently
npm install
```
Create a store in the [Opencals dashboard](https://opencals.com) and choose
the **Fluently** dataset preset (`language_school`). It seeds the six
languages, campus and online group classes with level variants, private
lessons, the free trial, teachers, both campuses, the online classroom with
its link, extras and placement questions. Then grab a Storefront API key
(see [Authentication](/docs/authentication)) and set `OPENCALS_API_KEY`,
`AUTH_SECRET` and `NEXT_PUBLIC_SITE_URL` in `.env.local`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm run dev
```
Your classes, teachers and timetable render immediately.
Push to your own repo and [deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fletsopencals%2Ftemplate-fluently\&env=OPENCALS_API_KEY,AUTH_SECRET\&envDescription=API%20key%20from%20your%20Opencals%20dashboard%20and%20a%20random%20secret%20for%20auth\&project-name=fluently\&repository-name=template-fluently),
or host anywhere Next.js runs.
Pages are server-rendered, so their API calls carry no browser `Origin`.
Leave **allowed origins** empty on the key you give this template, or those
calls are rejected.
Set **Storefront Base URL** in your dashboard (Settings → API) to your
deployed domain. Every customer link (view, reschedule, cancel, leave
feedback) is built as `{storefrontBaseUrl}/link/{token}`, so if it still
points at localhost those emailed links go nowhere.
## Make it yours
MIT-licensed: rebrand it, restructure it, or use it for client work. All copy
lives in `lib/site-config.ts`: hero, format descriptions, the level quiz, how it
works, testimonials, FAQ and values. Every colour and font is a Tailwind v4
`@theme` token in `app/globals.css`:
```css theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
@theme {
--color-bg: #FBF8F3; /* warm paper */
--color-ink: #16131F; /* ink + outlines */
--color-primary: #7C5CFF; /* grape */
--color-sun: #FFC93C; /* sunflower accent */
}
```
Building a different school site with an AI agent? The
[build-booking-site skill](/docs/ai-agents/build-booking-site-skill) has a
`classes-and-lessons` reference with the full pattern.
# Frisor — barbershop booking template
Source: https://opencals.com/docs/open-source/frisor
Free, open-source barbershop booking website template. Next.js 15, Stripe checkout, staff and multi-location booking, add-ons, customer accounts — built on the Opencals Storefront API.
A complete barbershop booking website you can clone and deploy today. Frisor is
the barbershop counterpart to [Haar](/docs/open-source/haar): same
[Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) underneath, a different vertical and a
dark editorial design. Every screen — services, barber picker, time slots,
add-ons, cart, checkout, customer account — is built with the public API.
template-frisor-sage.vercel.app
letsopencals/template-frisor (MIT)
One-click deploy
## What's inside
| Feature | Notes |
| - | - |
| Service catalog | Driven by the Products + Product Collections endpoints — Classic Cut, Beard Trim, Hot Towel Shave, Cut + Beard, Kids Cut |
| Barber & multi-location booking | Staff Members + Locations endpoints — three shops out of the box |
| Real-time availability | Appointments endpoints |
| Add-ons at checkout | Add-On endpoints — hot towel finish, beard oil, eyebrow trim, in one transaction |
| Cart & Stripe checkout | Cart + Checkout + Payment endpoints, deposits supported |
| Customer accounts | Self-Service endpoints — bookings, reschedules, cancellations |
| Stack | Next.js 15, TypeScript, Tailwind v4, `@opencals/storefront-sdk` |
## Get started
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
git clone https://github.com/letsopencals/template-frisor.git
cd template-frisor
npm install
```
Create a store in the [Opencals dashboard](https://opencals.com), grab a
Storefront API key (see [Authentication](/docs/authentication)), and set it in
`.env.local`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm run dev
```
Your shop's real services, barbers, and availability render immediately.
Push to your own repo and
[deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fletsopencals%2Ftemplate-frisor),
or host anywhere Next.js runs.
## Make it yours
The template is MIT-licensed: rebrand it, restructure it, or use it as a
starting point for client work. Every colour and font is a CSS variable — the
deep-green-and-gold palette and the Fraunces display font are defined in one
`@theme` block. Agencies can run one codebase against multiple stores — each
deployment just points at a different API key. To go deeper than theming,
follow the [booking page tutorial](/docs/guides/build-a-booking-page) to understand
the API calls behind each screen.
# Haar — salon booking template
Source: https://opencals.com/docs/open-source/haar
Free, open-source beauty salon booking website template. Next.js 15, Stripe checkout, staff and multi-location booking, customer accounts — built on the Opencals Storefront API.
A complete salon booking website you can clone and deploy today. Haar is the
reference implementation of the [Storefront API](/docs/api-reference/introduction):
every screen — services, staff picker, time slots, cart, checkout, customer
account — is built with the public API and the
[TypeScript SDK](/docs/guides/storefront-sdk).
template-haar.vercel.app
letsopencals/template-haar (MIT)
One-click deploy
## What's inside
| Feature | Notes |
| - | - |
| Service catalog | Driven by the Products + Product Collections endpoints |
| Staff & multi-location booking | Staff Members + Locations endpoints |
| Real-time availability | Appointments endpoints |
| Cart & Stripe checkout | Cart + Checkout + Payment endpoints, deposits supported |
| Customer accounts | Self-Service endpoints — bookings, reschedules, cancellations |
| Stack | Next.js 15, TypeScript, Tailwind, `@opencals/storefront-sdk` |
## Get started
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
git clone https://github.com/letsopencals/template-haar.git
cd template-haar
npm install
```
Create a store in the [Opencals dashboard](https://opencals.com), grab a
Storefront API key (see [Authentication](/docs/authentication)), and set it in
`.env.local`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm run dev
```
Your salon's real services, staff, and availability render immediately.
Push to your own repo and
[deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fletsopencals%2Ftemplate-haar),
or host anywhere Next.js runs.
## Make it yours
The template is MIT-licensed: rebrand it, restructure it, or use it as a
starting point for client work. Agencies can run one codebase against multiple
stores — each deployment just points at a different API key. To go deeper than
theming, follow the [booking page tutorial](/docs/guides/build-a-booking-page) to
understand the API calls behind each screen.
# NOIR Drive — luxury car rental template
Source: https://opencals.com/docs/open-source/noir
Free, open-source luxury car rental website template. Next.js 15, multi-day booking by the day, delivery or garage collection, per-day extras, chauffeur packages, Stripe checkout and passwordless accounts, built on the Opencals Storefront API.
A complete luxury car-rental website you can clone and deploy today. NOIR Drive
is the first template built around **multi-day bookings**: each car is booked
**by the day**, from a pick-up date to a return date, and blocked for the whole
span. Start here if you rent cars, boats, equipment or villas by the day. It runs
on the same [Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) as [Haar](/docs/open-source/haar),
[Frisor](/docs/open-source/frisor), [Clear Care](/docs/open-source/clarity) and
[VOLT](/docs/open-source/volt).
It's set in Dubai (AED, `Asia/Dubai`) with an all-black fleet of twelve cars.
Two booking surfaces sit over one store: a **multi-day rental flow** for the
fleet, and a classic **chauffeur flow** with real drivers.
template-noir.vercel.app
letsopencals/template-noir (MIT)
One-click deploy
## Multi-day booking
Customers pick a car, choose pick-up and return dates on a range calendar that
reads the car's live availability, then choose *Deliver to my address* or
*Collect at the garage* and a handover window. Per-day extras (excess waiver,
additional driver) are priced × days, and fixed extras (child seat, prepaid
fuel) once. Driving licence and ID uploads and a deposit-and-terms checkbox are
regular checkout questions. The total is always **days × daily rate + extras**,
the same maths the engine charges.
## The rental model
This is the part worth copying even if you never ship NOIR Drive itself. It
uses existing Opencals features only.
| Piece | How it's modelled |
| - | - |
| A car | One staffless product in its own product pool (so a booking blocks it at both locations), `duration: 86400`, `allowCustomDuration: true`, `maxDuration`, `maxAttendees: 1` |
| Availability across midnight | A continuous 24/7 schedule. Bookings longer than 24 hours are validated against continuous availability |
| Date picker | `getCurrentAvailabilitiesMerged` **without** `duration`; the client checks that pick-up → return fits inside one range |
| The slot | Pick-up date 00:00 → return date 00:00 in the store timezone, sent in UTC |
| Handover window, flight number, collection address | Appointment [custom attributes](/docs/changelog/custom-attributes) |
| Delivery or collection | A `PHYSICAL` garage location and a `DELIVERY` location; the customer's address is captured at checkout |
| Per-day extras | Add-ons with `durationMultiplied` |
| Security deposit | Shown per car and accepted with a required checkbox question; taken at handover, never charged online |
Dubai pick-up 10 Nov → return 13 Nov becomes
`{ fromDate: '2026-11-09', fromTime: '20:00:00', toDate: '2026-11-12', toTime: '20:00:00' }`:
three days, three units, three × the daily rate.
Give each car its own product rather than a variant: variants don't block each
other, so the same car could be booked twice.
## What's inside
| Feature | Notes |
| - | - |
| Fleet and car pages | Category filters from collections, "available for your dates", a two-month availability calendar with booked days struck through |
| Rates | A full rates table (daily rate, deposit, km per day) |
| Multi-day booking | Range calendar, delivery or garage, handover windows, per-day and fixed extras, checkout questions |
| Chauffeur | Packages booked through the classic staff flow, with a preferred-car request |
| Cart & Stripe checkout | Cart + Checkout + Payment endpoints |
| Rental-aware accounts | Pick-up → return dates in the store timezone, day count, handover details; customers can change dates (same length) or cancel within policy |
| Passwordless accounts | 6-digit email login (password fallback), one-time email links via `/link/[token]` |
| Motion | Lenis smooth scrolling, text and image reveals, parallax, count-up specs; every effect has a reduced-motion fallback |
| SEO | Per-page metadata, Open Graph, `AutoRental` and `Product` structured data, `robots.txt`, `sitemap.xml` |
| Stack | Next.js 15, TypeScript, Tailwind v4, Framer Motion, `@opencals/storefront-sdk` v0.3.14 |
## Get started
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
git clone https://github.com/letsopencals/template-noir.git
cd template-noir
npm install
```
Create a store in the [Opencals dashboard](https://opencals.com) and choose
the **NOIR Drive** dataset preset (category Car rental). It seeds the
fleet on a 24/7 schedule, the garage and delivery locations, extras,
checkout questions, chauffeurs and packages. Then grab a Storefront API key
(see [Authentication](/docs/authentication)) and set `OPENCALS_API_KEY` and
`AUTH_SECRET` in `.env`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm run dev
```
Your cars, rates and availability render immediately.
Push to your own repo and
[deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fletsopencals%2Ftemplate-noir\&env=OPENCALS_API_KEY,AUTH_SECRET\&envDescription=API%20key%20from%20your%20Opencals%20dashboard%20and%20a%20random%20secret%20for%20auth\&project-name=noir-drive\&repository-name=template-noir),
or host anywhere Next.js runs.
Set **Storefront Base URL** in your dashboard (Settings → API) to your
deployed domain. Every customer link (view, reschedule, cancel, leave
feedback) is built as `{storefrontBaseUrl}/link/{token}`, so if it still
points at localhost those emailed links go nowhere.
## Make it yours
MIT-licensed: rebrand it, restructure it, or use it for client work. All
business copy lives in `lib/site-config.ts`: name, currency and timezone,
per-car specs and deposits, handover windows, delivery zones, FAQs and contact
details. Every colour and font is a Tailwind v4 `@theme` token in
`app/globals.css`:
```css theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
@theme {
--color-bg: #050505; /* page black */
--color-ink: #F4F1EA; /* warm ivory text */
--color-primary: #C8A96B; /* champagne accent */
}
```
Building a different rental site with an AI agent? The
[build-booking-site skill](/docs/ai-agents/build-booking-site-skill) has a
`rentals-multi-day` reference with the full pattern.
# Open-source templates
Source: https://opencals.com/docs/open-source/templates
Free, open-source booking website templates built on the Opencals Storefront API. Clone, customize, and deploy a production booking site in minutes.
Every template is a complete, production-grade booking website built on the
[Storefront API](/docs/api-reference/introduction) and the typed
[TypeScript SDK](/docs/guides/storefront-sdk). MIT-licensed — clone it, rebrand it,
ship it for yourself or your clients.
## How it works
Each template is a standalone Next.js repository on
[GitHub](https://github.com/letsopencals).
Create a store in the [Opencals dashboard](https://opencals.com) and set
your Storefront API key in the env vars. See
[Authentication](/docs/authentication).
One-click deploy to Vercel, or anywhere Next.js runs. Bookings, checkout,
and payments work out of the box.
## Templates
A free beauty-salon booking website: services, staff and multi-location
booking, customer accounts, and Stripe checkout. Next.js 15 + TypeScript.
**[Live demo ↗](https://template-haar.vercel.app)** · **[GitHub ↗](https://github.com/letsopencals/template-haar)**
A free barbershop booking website: services and variants, barber and
multi-location booking, add-ons at checkout, customer accounts, and Stripe
checkout. Dark editorial design — Next.js 15 + TypeScript.
**[Live demo ↗](https://template-frisor-sage.vercel.app)** · **[GitHub ↗](https://github.com/letsopencals/template-frisor)**
A free medical-clinic booking website: a Care Finder that starts from what's
going on, a single-page booking composer with pre-visit notes and photo
uploads, clinic or video visits across three clinics, Stripe checkout, and a
family care record in My Care. Calm, warm design — Next.js 15 + TypeScript.
**[Live demo ↗](https://template-clarity.vercel.app)** · **[GitHub ↗](https://github.com/letsopencals/template-clarity)**
A free padel & squash club booking website: a multi-court booking grid with
custom durations, coach-led training with group capacity, add-ons and custom
questions at checkout, passwordless customer accounts, and Stripe checkout.
The first template built on staffless resources rather than staff-led
appointments. Dark technical design — Next.js 15 + TypeScript.
**[Live demo ↗](https://template-volt.vercel.app)** · **[GitHub ↗](https://github.com/letsopencals/template-volt)**
A free luxury car-rental website: a fleet booked by the day with a range
calendar on live availability, delivery or garage collection, per-day extras,
licence and ID uploads at checkout, chauffeur packages with real drivers,
rental-aware customer accounts, and Stripe checkout. The first template built
on multi-day bookings. Black cinematic design — Next.js 15 + TypeScript.
**[Live demo ↗](https://template-noir.vercel.app)** · **[GitHub ↗](https://github.com/letsopencals/template-noir)**
A free language-school website: group classes with level variants and live
seats left, private lessons, a free trial, a week-ahead timetable, teacher
profiles, campuses plus an online classroom with a Join button, passwordless
student accounts, and Stripe checkout. The first template built for schools
and courses. Colorful, playful design — Next.js 15 + TypeScript.
**[Live demo ↗](https://template-fluently.vercel.app)** · **[GitHub ↗](https://github.com/letsopencals/template-fluently)**
A free villa-rental website: ten villas booked by the night with fixed
check-in and check-out, a calendar on live availability, guest and arrival
details, per-stay extras, trip-aware customer accounts with date changes and
cancellation, passwordless login, and Stripe checkout. The first template
built for nightly stays. Warm, quiet design — Next.js 15 + TypeScript.
**[Live demo ↗](https://template-bali-villas.vercel.app)** · **[GitHub ↗](https://github.com/letsopencals/template-bali-villas)**
Booking courts, bays, lanes or rooms rather than people? Start from
[VOLT](/docs/open-source/volt), built on staffless resources. Renting by the day
(cars, boats, equipment)? Start from
[NOIR Drive](/docs/open-source/noir). Renting villas, cabins or rooms by the
night? Start from [Bali Villa Rentals](/docs/open-source/bali-villas). Running a school or courses? Start from
[Fluently](/docs/open-source/fluently). More are on the way: spa, fitness, and tattoo studio are next.
Want one prioritized? [Tell us](https://opencals.com/contact).
## Quick start
```bash Clone Haar theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
git clone https://github.com/letsopencals/template-haar.git
cd template-haar
npm install
```
```bash Install the SDK only theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm install @opencals/storefront-sdk
```
Build something custom instead? Start from the
[booking page tutorial](/docs/guides/build-a-booking-page).
# VOLT — padel & squash club template
Source: https://opencals.com/docs/open-source/volt
Free, open-source padel and squash club booking website template. Next.js 15, a multi-court booking grid with custom durations, coach-led training, Stripe checkout, passwordless accounts — built on the Opencals Storefront API.
A complete padel & squash club booking website you can clone and deploy today.
VOLT is the first template built around **staffless resources** rather than
staff-led appointments: courts are independent, exclusively-booked units, so
this is the one to start from if you are booking courts, bays, lanes, studios
or rooms rather than people. It runs on the same
[Storefront API](/docs/api-reference/introduction) and typed
[TypeScript SDK](/docs/guides/storefront-sdk) as [Haar](/docs/open-source/haar),
[Frisor](/docs/open-source/frisor) and [Clear Care](/docs/open-source/clarity).
Two booking surfaces sit over one store: an in-page **court booking grid** for
rentals, and a classic **coach-led training flow** for lessons and group
classes.
template-volt.vercel.app
letsopencals/template-volt (MIT)
One-click deploy
## The court booking grid
The centrepiece. One grid shows **every court and every open slot** for a day
at once — courts down the rows, time across the columns — with a Padel /
Squash toggle that changes which courts fill the grid. Each court is its own
resource, so booking one leaves every other court free.
Courts book in 30-minute steps up to two hours. The grid walks each court's row
for runs of consecutive free slots and composes them into the 30 / 60 / 90 /
120-minute options actually available from the cell you tapped, pricing each
one live. Hovering a duration highlights the spanned cells; a Confirm button
commits. That logic lives in a `use-court-grid` hook rather than in the page,
which is the first place to look if you price peak hours differently or cap
rental length.
## Coach-led training
A separate flow over the same store, for lessons and classes. Filter the
catalog by sport, level (Starter → Advanced) and format (1-on-1 or group), pick
a coach — or **"Any coach — next available"**, which leads the picker — then a
date and time. Group classes show live **"N left"** capacity and disappear when
they fill.
## Two resource models, one store
This is the part worth copying even if you never ship VOLT itself.
| Model | Rule | Capacity | Reads as |
| - | - | - | - |
| Courts (rentals) | `Self` — staffless resource | `maxAttendees: 1` | "Padel Court 3 — 90 min" |
| Trainings (lessons) | `Staff` — assigned to a coach | `maxAttendees: 1` or more | "Intermediate Group — with Coach Marta" |
Orders and appointment detail read naturally either way, so nothing downstream
has to decode a court that was faked as a staff member.
## What's inside
| Feature | Notes |
| - | - |
| Multi-court booking grid | Products + Appointments endpoints — every court and slot for a day in one view, padel/squash toggle |
| Custom durations | 30-minute steps to two hours, composed from consecutive free slots and priced live |
| Two sports, one store | Product Collections endpoints — a third sport is a data change, not a code change |
| Coach-led training | Staff Members endpoints — filter by sport, level and format; "Any coach" first |
| Group capacity | Live "N left" counts from `maxAttendees` |
| Add-ons at checkout | Racket and ball rental out of the box |
| Custom questions | Checkout questions configured in the dashboard, saved to the appointment |
| Cart & Stripe checkout | Cart + Checkout + Payment endpoints, deposits supported |
| Passwordless accounts | Self-Service endpoints — 6-digit email login (password fallback), auto-login after checkout, one-time email links via `/link/[token]` |
| Video-ready hero | Upgrades to a looping video when `public/videos/hero.mp4` exists, still image otherwise |
| SEO | Per-page metadata, Open Graph, `SportsActivityLocation` structured data, `robots.txt`, `sitemap.xml` |
| Stack | Next.js 15, TypeScript, Tailwind v4, Framer Motion, `@opencals/storefront-sdk` |
## Get started
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
git clone https://github.com/letsopencals/template-volt.git
cd template-volt
npm install
```
Create a store in the [Opencals dashboard](https://opencals.com) — choose the
**VOLT Padel & Squash** dataset preset to seed the club's courts, coaches,
training classes, add-ons and venue — then grab a Storefront API key (see
[Authentication](/docs/authentication)) and set `OPENCALS_API_KEY` and
`AUTH_SECRET` in `.env`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm run dev
```
Your real courts, coaches and availability render immediately.
Push to your own repo and
[deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fletsopencals%2Ftemplate-volt\&env=OPENCALS_API_KEY,AUTH_SECRET\&envDescription=API%20key%20from%20your%20Opencals%20dashboard%20and%20a%20random%20secret%20for%20auth\&project-name=volt\&repository-name=template-volt),
or host anywhere Next.js runs.
Set **Storefront Base URL** in your dashboard (Settings → API) to your
deployed domain. Every customer link — view, reschedule, cancel, leave
feedback — is built as `{storefrontBaseUrl}/link/{token}`, so if it still
points at localhost those emailed links go nowhere.
## Make it yours
MIT-licensed: rebrand it, restructure it, or use it as a starting point for
client work. All club-specific copy lives in `lib/site-config.ts` — club name,
the two sports and their price hints, hero, coaches, gallery, stats,
testimonials, FAQs, address and hours. Every colour and font is a Tailwind v4
`@theme` token in `app/globals.css`, so components keep working when you change
values:
```css theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
@theme {
--color-bg: #0A0A0A; /* near-black page background */
--color-primary: #FF4A1C; /* electric-orange accent */
--font-display: 'JetBrains Mono', ui-monospace, monospace;
}
```
Keep the accent aligned with your store's storefront primary colour so booking
emails match the site. Agencies can run one codebase against multiple stores —
each deployment points at a different API key. To go deeper than theming,
follow the [booking page tutorial](/docs/guides/build-a-booking-page) to see the API
calls behind each screen.
# Quickstart
Source: https://opencals.com/docs/quickstart
What the Opencals Storefront API is, how to get an API key, and how to make your first request.
The Opencals Storefront API is a REST booking API for developers. It exposes everything a booking website needs — services, real-time availability, staff members, locations, carts, checkout, and payments — so you can build a fully custom booking experience on any stack while Opencals handles scheduling logic, conflict prevention, and order management.
Everything you configure in the Opencals dashboard is available over the API.
## Base URL
All Storefront API endpoints live under a single base URL:
```text theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
https://api.opencals.com
```
Endpoint paths are prefixed with `/storefront` — for example `GET /storefront/products` lists your bookable services. Responses are JSON with camelCase property names.
## Make your first request
1. Sign up at [opencals.com](https://opencals.com) and set up your store — services, staff members, locations, and schedules.
2. In the dashboard, go to **Settings → API Keys** and create a new **Storefront API Key**.
3. Each key is scoped to your store and starts with `sfk_`.
Keep the key server-side (for example in an environment variable). It identifies your store and should never be shipped to the browser.
List your store's active services with a single call:
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
curl "https://api.opencals.com/storefront/products" \
-H "X-Api-Key: sfk_your_key_here"
```
The response is a paginated collection:
```json theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
{
"data": [
{
"id": "9c6f7c2e-…",
"title": "Haircut",
"duration": 3600,
"price": 4500,
"maxAttendees": 1
}
],
"meta": {
"page": 1,
"take": 50,
"itemCount": 12,
"pageCount": 1,
"hasPreviousPage": false,
"hasNextPage": false
}
}
```
You can also try every endpoint directly in the [interactive API reference](/docs/api-reference/introduction).
## Pagination
List endpoints accept `page` (1-indexed) and `take` (items per page, maximum 100) query parameters and return a `meta` object with `page`, `take`, `itemCount`, `pageCount`, `hasPreviousPage`, and `hasNextPage`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
curl "https://api.opencals.com/storefront/products" \
-H "X-Api-Key: sfk_your_key_here" \
--url-query "page=2" \
--url-query "take=20"
```
## Rate limits
Requests are rate-limited per API key:
| Category | Limit |
| - | - |
| General | 100 requests / 60 seconds |
| Authentication | 20 requests / 10 seconds |
| Password reset | 5 requests / hour |
| Registration | 10 requests / minute |
When you exceed a limit the API responds with `429 Too Many Requests`.
## Timezones
All date and time values in requests and responses are in **UTC**. Availability endpoints accept a `timezone` query parameter (for example `America/New_York`) to return slots in the user's local timezone. Convert user-selected times back to UTC before creating appointments.
## Next steps
API keys, customer tokens, and error responses.
The full flow from listing services to a confirmed booking.
The typed TypeScript client for this API.
Every endpoint, parameter, and response shape.