@openings-link/react

Open-source React library for building booking interfaces with Openings.

Why Openings?

Most booking platforms treat a business as a flat list of time slots. That works for a solo freelancer — but falls apart the moment you have multiple locations or a team.

Openings is built around the way real service businesses actually work:

Installation

npm install @openings-link/react @openings-link/react-ui

@openings-link/react — Headless hooks, state machine, and types. Zero dependencies, React 18+ peer dep only.

@openings-link/react-ui — Themed, drop-in booking components built on the headless core. Styled with CSS custom properties (no Tailwind, no CSS modules).

Using the Real API (CORS-safe)

This example app includes a same-origin proxy route at /api/proxy/* so browser requests do not hit https://api.openings.link directly.

When integrating in your own app, set apiBase to your proxy path:

<BookingWidget
  business="your-business-handle"
  apiBase="/api/proxy"
/>

For full templates (Next.js, Remix, Express), seeproxy-templates.md.

Quick Start

Drop-in Widget

One component, one prop. Connect to a live Openings business by its handle.

import { BookingWidget } from "@openings-link/react-ui";

<BookingWidget
  business="your-business-handle"
  theme={{ accent: "#8B5CF6" }}
  on={{
    onBookingComplete: (result) => {
      console.log("Booked!", result);
    },
  }}
/>

Headless (Custom UI)

Use the hooks directly to build any UI. All booking state and actions are exposed through useBookingFlow().

import { OpeningsProvider, useBookingFlow } from "@openings-link/react";

<OpeningsProvider business="your-business-handle">
  <MyBookingUI />
</OpeningsProvider>

function MyBookingUI() {
  const {
    step,            // schedule | openings | review | verify | confirm | service-request
    schedules,       // available locations
    services,        // filtered by staff in member mode
    members,         // staff for selected schedule
    selectedServices,// user's selected services
    memberOpenings,  // time slots by staff (or by location in staff mode)
    selectSchedule,
    selectService,
    removeService,
    selectSlot,
    goToReview,
    goBack,
    canGoBack,
  } = useBookingFlow();
}

Booking Layers

Openings organizes booking around four layers. Understanding these is key to using the library — and what makes Openings powerful for multi-location, multi-staff businesses.

Business

The top-level entity. A business has a unique handle (slug) like "demo" and is the only required prop.

Schedules (Locations)

A business has one or more schedules. Each schedule is a bookable unit — usually a physical location, but it can also be an online schedule (no address). A barbershop with two studios has two schedules.

Demo Barbershop ├── East Village (154 Orchard St, New York) └── SoHo (129 Grand St, New York)

When a business has multiple schedules, the widget shows a location picker first. With only one schedule, it skips straight to booking.

Staff (Members)

Each schedule has staff members assigned to it. A staff member can work at multiple locations. When a customer picks a time slot, they're booking with a specific person at a specific place.

East Village SoHo ├── John ←─────────── John (works at both) ├── Amy ←─────────── Amy (works at both) └── Noah ├── Rose └── Natalie

Staff booking mode: Pass memberId to show a specific staff member's availability across all their locations. Only their services are listed, and time slots are grouped by location.

Services

Services belong to the business, but each staff member offers different services. A colorist has different services than a barber. In location mode the dropdown shows all services available there. In staff mode it shows only that person's services.

John's services Amy's services ├── Short Haircut ├── Short Haircut ├── Medium Haircut ├── Medium Haircut ├── Long Haircut ├── Long Haircut └── Color └── Color

How It All Fits Together

Business └── Schedule (location) ├── Staff member A │ ├── Service 1 │ └── Service 2 └── Staff member B ├── Service 2 └── Service 3
Entry propsCustomer sees
business only (multi)Location → services → date/time → book
business only (single)Services → date/time → book
business + memberIdStaff's services → date/time by location → book
business + scheduleIdServices → date/time → book (location locked)

Booking Flow

The step machine enforces a linear flow with back navigation:

schedule → openings → review → verify → confirm └→ service-request → confirm

schedule — pick a location (skipped if one schedule or entry prop set).
openings — pick service, date, and time slot.
review — confirm selection.
verify — phone/email lookup. New customers enter name + verification code. Returning customers book directly.
confirm — booking confirmation.
service-request — consultation form with optional photo uploads. Triggered by services marked hasConsultation.

Props Reference

All props accepted by <BookingWidget />:

PropTypeDescription
business*stringRequired. The business handle (slug) to load. This is the only required prop.
apiBasestringAPI base URL. Defaults to "https://api.openings.link". Override for self-hosted or staging environments.
scheduleIdstringPre-select a specific schedule. Skips the location picker and jumps straight to service/time selection.
memberIdstringPre-select a specific staff member. Shows only their services and groups time slots by location.
themeBookingThemeCustomize the widget appearance — accent color, border radius, font, and light/dark mode.
labelsPartial<BookingLabels>Override any user-facing string for i18n or custom copy.
apiClientApiClientInject a custom API client. Useful for testing with mock data or wrapping requests.
onOpeningsCallbacksEvent callbacks for booking lifecycle events.
onConsultationRequest(member, services) => voidCalled when a user clicks "Send a Request" on a consultation service. If omitted, the built-in service request form is shown.
classNamestringCSS class name applied to the root container element.

BookingTheme

{
  accent?: string;      // Primary color (default: "#000000")
  radius?: number;      // Border radius in px (default: 8)
  fontFamily?: string;  // Font family (default: system)
  mode?: "light" | "dark" | "auto";
}

OpeningsCallbacks

{
  onBookingComplete?: (result) => void;
  onServiceRequestComplete?: (result) => void;
  onStepChange?: (from, to) => void;
  onError?: (error) => void;
  onScheduleSelect?: (schedule) => void;
  onServiceSelect?: (service) => void;
  onSlotSelect?: (slot) => void;
  onVerificationSent?: (method) => void;
  onVerificationComplete?: (customerId) => void;
  onConsultationRequest?: (request) => void;
}

See live demos → Examples