# How to add booking to a Next.js app with a REST API

> A hands-on guide to adding appointment booking to a Next.js app using a booking API — real-time availability, cart, checkout, and code you can copy.

URL: https://opencals.com/blog/add-booking-to-nextjs-app
Author: Stanislav Tyshchenko
Published: 2026-07-21
Updated: 2026-10-28
Categories: tutorial

Adding a booking page to a Next.js app looks like a weekend project until you start it. The UI is the easy part. The hard part is everything the calendar needs to know before it can show a single open slot: which staff are working, how long the service takes, what's already booked, how much buffer to leave between appointments, and whether two people can hold the same slot at once. That logic is where booking projects quietly turn into month-long ones.

This guide takes the other path. Instead of building the scheduling engine yourself, you call a booking API for the availability, cart, and payment logic, and you keep full control of the Next.js frontend. I built [Opencals](https://opencals.com/) as exactly that kind of API, so the code here uses it — but the shape of the problem is the same whichever backend you pick, and I'll flag the parts that are universal.

> **Key takeaway**
>
> The frontend of a booking flow is a few forms and a calendar grid. The backend is real-time availability across staff, services, and existing bookings — plus payments. Calling a booking API lets you own the first part and hand off the second.

## What "add booking" actually involves

Before any code, it helps to name the pieces. A working booking flow has four moving parts, and most half-finished projects stall because they underestimated one of them.

1. **A service catalog**: What can be booked — a 60-minute session, a color treatment, a group class — each with its own duration, price, and the staff who can deliver it. This is your booking homepage data.
2. **Real-time availability**: Given a service and a date range, which slots are actually open? This has to account for existing bookings, staff hours, buffer time, and multi-staff conflicts. It is the single hardest piece to build well.
3. **A cart and checkout**: Holding a slot while the customer enters details and pays, then confirming the booking only if payment succeeds. Slots are a scarce resource, so this needs to be race-safe.
4. **Post-booking management**: Confirmation, reminders, and letting customers reschedule or cancel without emailing you. Easy to skip on day one, painful to add later.

If you build all four yourself, you're writing a scheduling engine. If you call an API, you're wiring four typed methods into React components. This guide does the second.

## Prerequisites

You need a Next.js 14+ app using the App Router, Node 18+, and a booking API key. With Opencals that's a private storefront key (prefixed `spk_`), created in the dashboard under Users → your user → API Keys; any booking API will have an equivalent. Install the SDK:

```bash
npm i @opencals/storefront-sdk
```

Then set the key in your environment. Keep it server-side — a private storefront key can create real bookings, so it does not belong in client bundles.

```bash
# .env.local
OPENCALS_API_KEY=spk_your_key_here
```

> **Why server-side**
>
> Anything that creates a booking or a payment should run in a Route Handler or Server Action, never in a client component. The browser calls your Next.js API routes; your routes call the booking API with the secret key. This is the same pattern you'd use for a Stripe secret key.

## Step 1 — Initialise the SDK once

Create a small module that configures the client. Everything server-side imports from here, so the key lives in exactly one place.

```typescript
// lib/opencals.ts
import { setupOpencals } from "@opencals/storefront-sdk";
import { nextBuyerIp } from "@opencals/storefront-sdk/next";

export const opencals = setupOpencals({
  apiKey: process.env.OPENCALS_API_KEY!, // spk_…
  buyerIp: nextBuyerIp,
});
```

`nextBuyerIp` forwards each visitor's IP on sign-in, cart and checkout calls, so the API rate-limits per visitor instead of treating your server as one very busy customer.

If you prefer raw `fetch` over an SDK, the same call is a `GET` with the key in an `X-Api-Key` header — the SDK just gives you typed responses. Both work; typing saves you from guessing the shape of a nested availability object at 11pm.

```bash
curl https://api.opencals.com/storefront/products \
  -H "X-Api-Key: spk_your_key"
```

## Step 2 — List services on a Server Component

The App Router lets you fetch on the server and stream HTML, which is ideal for a service catalog — it's public data, good for SEO, and doesn't need to be interactive.

```typescript
// app/book/page.tsx
import { opencals } from "@/lib/opencals";
import { ProductService } from "@opencals/storefront-sdk";
import { ServiceCard } from "./service-card";

export default async function BookPage() {
  const { data: products } = await ProductService.listProducts({ opencals });

  return (
    <main className="grid gap-4">
      {products.map((service) => (
        <ServiceCard key={service.id} service={service} />
      ))}
    </main>
  );
}
```

`products` is fully typed — `service.variants` carries per-variant duration and price, `service.staff` the practitioners who can deliver it. You render, you don't compute. The booking API already resolved which staff map to which service.

## Step 3 — Fetch real-time availability

This is the endpoint that would have taken you weeks to write. You pass a service, a date range, and optionally a staff member; you get back the slots that are genuinely open right now.

```typescript
// app/api/availability/route.ts  (Route Handler)
import { NextRequest, NextResponse } from "next/server";
import { opencals } from "@/lib/opencals";
import { AppointmentService } from "@opencals/storefront-sdk";

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const productId = searchParams.get("productId")!;
  const from = searchParams.get("from")!;
  const to = searchParams.get("to")!;

  const { data: slots } = await AppointmentService.getAvailability({
    opencals,
    productId,
    from,
    to,
  });

  return NextResponse.json({ slots });
}
```

The returned `slots` already exclude times that are booked, outside staff hours, or too close to another appointment given the service's buffer. Your calendar component fetches this route when the user picks a date and renders whatever comes back — no client-side conflict math.

> **The universal part**
>
> Every booking backend worth using exposes an availability endpoint like this. If a tool only gives you raw calendar events and expects you to compute open slots yourself, you haven't offloaded the hard part — you've just moved it. Availability calculation is the feature to evaluate first.

## Step 4 — Hold the slot with a cart

A slot is a scarce resource. Between "customer clicked 2pm" and "customer paid," you don't want someone else grabbing it, and you don't want to create a real booking that might never be paid for. A cart holds the slot for a short window.

```typescript
// app/api/cart/route.ts
import { NextRequest, NextResponse } from "next/server";
import { opencals } from "@/lib/opencals";
import { CartService } from "@opencals/storefront-sdk";

export async function POST(req: NextRequest) {
  const { productId, variantId, slot } = await req.json();

  const { data: cart } = await CartService.addItem({
    opencals,
    productId,
    variantId,
    startTime: slot,
  });

  return NextResponse.json({ cartId: cart.id, cart });
}
```

Store `cartId` in the user's session (a cookie or your session store). It's the handle you'll pass to checkout.

## Step 5 — Checkout and confirm

Checkout turns a held cart into a paid booking. The important property here is idempotency: payment flows fail and retry in the real world, and a repeated request must not create two bookings or charge twice.

```typescript
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { opencals } from "@/lib/opencals";
import { CheckoutService } from "@opencals/storefront-sdk";

export async function POST(req: NextRequest) {
  const { cartId, customer } = await req.json();

  // 1. Create a payment intent for the cart total
  const { data: intent } = await CheckoutService.initiate({
    opencals,
    cartId,
    customer,
  });

  // 2. Client confirms the payment with intent.clientSecret,
  //    then you confirm the booking:
  const { data: booking } = await CheckoutService.confirm({
    opencals,
    cartId,
    paymentIntentId: intent.id,
  });

  return NextResponse.json({ booking });
}
```

The two-step split — `initiate` returns a payment intent, the browser confirms the card, then `confirm` finalises the booking — is what keeps a failed card from leaving a phantom appointment on the calendar. The slot only becomes a real booking once money has actually moved.

## Step 6 — Let customers manage their own bookings

The reschedule-and-cancel flow is the one teams skip first and regret most, because without it every change becomes a support message. These endpoints work off a customer token issued by your own auth flow, not the store key — the customer can only see and touch their own bookings.

```typescript
const { data: bookings } = await AppointmentService.listCustomerBookings({
  opencals,
  customerToken, // issued by your auth, scoped to one customer
});

await AppointmentService.reschedule({
  opencals,
  customerToken,
  bookingId,
  newStartTime,
});
```

Wire these into an account page and the reschedule requests that used to hit your inbox now resolve themselves.

## Putting it together

The full flow, from the frontend's point of view, is five typed calls:

1. **List services**: ProductService.listProducts() → render the catalog on a Server Component.
2. **Check availability**: AppointmentService.getAvailability() → the open slots for a date, computed server-side.
3. **Add to cart**: CartService.addItem() → hold the chosen slot while the customer checks out.
4. **Initiate checkout**: CheckoutService.initiate() → a payment intent the browser confirms.
5. **Confirm booking**: CheckoutService.confirm() → the slot becomes a real, paid booking.

Everything hard about scheduling — the availability math, the race conditions, the payment-to-booking handoff — lives behind those calls. Your Next.js app stays a frontend: forms, a calendar grid, and route handlers that proxy to the API with your secret key.

## When not to use an API for this

Honest caveat, because not every project needs this. If you're a solo consultant booking 1:1 calls and you don't take payment up front, a hosted tool like [Calendly](https://opencals.com/blog/compare/calendly) is simpler and you shouldn't build anything. An API earns its keep when you need a custom-branded flow, multiple staff or locations, payments at booking, or booking embedded inside a product you already own. That's when owning the frontend while renting the backend is the right trade — and the links below cover the endpoint surface, the architecture, and the typed SDK.

## Where to go from here

- [Booking API guide](https://opencals.com/blog/booking-api): The endpoint-level reference: auth model, core routes, and the full booking flow in raw REST.
- [Booking SDK](https://opencals.com/blog/booking-sdk): The typed TypeScript SDK used in this guide — services, availability, cart, and checkout end to end.
- [Headless booking system](https://opencals.com/blog/headless-booking-system): The architecture behind building a custom booking frontend on any stack.
- [Opencals for developers](https://opencals.com/developers): API docs, the OpenAPI spec, and how to get a storefront key.
- [Build a booking website](https://opencals.com/blog/build-a-booking-website): Three approaches compared, from no-code to a fully custom build.

## FAQ

## Frequently asked questions

### Can I add booking to Next.js without a third-party API?

Yes, but you're then building a scheduling engine: real-time availability across staff and services, buffer logic, race-safe slot holding, and payment-to-booking handoff. For a single service with no payments that's doable in a weekend. For anything with multiple staff, locations, or paid bookings, the availability and concurrency logic is where most of the effort goes — which is what an API removes.

### Where do I keep the booking API key in Next.js?

Server-side only — in an environment variable read by Route Handlers or Server Actions. A storefront key can create real bookings and charges, so it must never ship in a client bundle. The browser calls your own Next.js API routes; those routes call the booking API with the secret key.

### Does this work with the App Router and Server Components?

Yes. Public reads like the service catalog fit Server Components and stream as HTML, which is good for SEO. Anything that mutates — cart, checkout, reschedule — goes in Route Handlers or Server Actions so the secret key stays on the server.

### How is a booking API different from a calendar API like Google Calendar?

A calendar API stores and returns events. A booking API computes availability — it knows service durations, staff assignments, buffers, and existing bookings, and returns the slots a customer can actually book. With a calendar API you'd still have to write all of that logic yourself on top of raw events.

### Can customers reschedule or cancel on their own?

Yes, via customer-scoped endpoints that use a token from your own auth flow rather than the store key. Wire them into an account page and self-service reschedule and cancel replace the support messages you'd otherwise field manually.
