Run this as a tiny server (or serverless function). Never call Cal.com with secrets from the browser.
// server/index.ts
import express from "express";
import fetch from "node-fetch";
import cors from "cors";
const app = express();
app.use(cors());
app.use(express.json());
const CAL_API = "<https://api.cal.com>";
const CAL_VERSION_SLOTS = "2024-09-04"; // v2 /slots requires this header
const CAL_VERSION_BOOK = "2024-08-13"; // v2 /bookings requires this header
const CAL_TOKEN = process.env.CAL_TOKEN!; // Personal API key (starts with cal_) or managed-user access token
// GET /api/slots?eventTypeId=123&date=2025-10-01&tz=America/New_York
app.get("/api/slots", async (req, res) => {
const { eventTypeId, username, eventTypeSlug, date, tz } = req.query as any;
// choose either ?eventTypeId=… OR ?username=…&eventTypeSlug=…
const qs =
eventTypeId
? `eventTypeId=${eventTypeId}`
: `eventTypeSlug=${encodeURIComponent(eventTypeSlug)}&username=${encodeURIComponent(username)}`;
const start = date; // YYYY-MM-DD
const end = new Date(`${date}T00:00:00Z`); // next day
end.setUTCDate(end.getUTCDate() + 1);
const endISO = end.toISOString().slice(0, 10); // YYYY-MM-DD
const url = `${CAL_API}/v2/slots?${qs}&start=${start}&end=${endISO}&timeZone=${encodeURIComponent(tz)}`;
const r = await fetch(url, {
headers: {
Authorization: `Bearer ${CAL_TOKEN}`,
"cal-api-version": CAL_VERSION_SLOTS,
},
});
const json = await r.json();
res.status(r.status).json(json);
});
// POST /api/book { eventTypeId, start, name, email, timeZone }
app.post("/api/book", async (req, res) => {
const body = req.body; // {eventTypeId:number, start:string(ISO UTC), name, email, timeZone}
const r = await fetch(`${CAL_API}/v2/bookings`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${CAL_TOKEN}`,
"cal-api-version": CAL_VERSION_BOOK,
},
body: JSON.stringify({
eventTypeId: body.eventTypeId,
start: body.start, // e.g. "2025-10-01T14:00:00Z"
attendee: { name: body.name, email: body.email },
timeZone: body.timeZone,
// optional: notes, answers, location, metadata, etc.
}),
});
const json = await r.json();
res.status(r.status).json(json);
});
app.listen(4000, () => console.log("API on <http://localhost:4000>"));
Env:
CAL_TOKEN=cal_xxxxxx_your_api_key
Docs: Cal.com v2 /slots & /bookings plus required headers. (Cal)
Install shadcn’s calendar deps (it wraps react-day-picker):
npm i date-fns react-day-picker
# (and your shadcn/ui setup; assume Calendar is generated at "@/components/ui/calendar")
// Book.tsx (React, CRA/Vite)
import { useEffect, useMemo, useState } from "react";
import { Calendar } from "@/components/ui/calendar";
import { formatISO, parseISO } from "date-fns";
const API = "<http://localhost:4000>"; // your Express base
const EVENT_TYPE_ID = 123; // <-- your Cal.com eventTypeId
export default function Book() {
const [date, setDate] = useState<Date>();
const [slots, setSlots] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
useEffect(() => {
if (!date) return;
const d = date.toISOString().slice(0, 10); // YYYY-MM-DD
setLoading(true);
fetch(`${API}/api/slots?eventTypeId=${EVENT_TYPE_ID}&date=${d}&tz=${encodeURIComponent(timeZone)}`)
.then(r => r.json())
.then(({ slots }) => setSlots(slots ?? []))
.finally(() => setLoading(false));
}, [date, timeZone]);
return (
<div className="mx-auto grid max-w-4xl gap-6 md:grid-cols-2">
<Calendar mode="single" selected={date} onSelect={setDate} />
<div className="space-y-4">
<h3 className="text-lg font-semibold">Available times</h3>
{loading && <p className="text-sm opacity-70">Loading…</p>}
<div className="flex flex-wrap gap-2">
{slots.map((s) => (
<button
key={s}
className="rounded-md border px-3 py-2 hover:bg-accent"
onClick={() => pickSlot(s)}
>
{s}
</button>
))}
</div>
</div>
</div>
);
async function pickSlot(localTime: string) {
if (!date) return;
// Convert local slot (e.g. "14:00") + selected date to UTC ISO for Cal.com
const local = new Date(`${date.toISOString().slice(0,10)}T${localTime}:00`);
const startUTC = new Date(
local.getTime() - (local.getTimezoneOffset() * 60000)
).toISOString().replace(/\\.\\d{3}Z$/, "Z");
const name = prompt("Your name") ?? "";
const email = prompt("Your email") ?? "";
const r = await fetch(`${API}/api/book`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
eventTypeId: EVENT_TYPE_ID,
start: startUTC,
name, email,
timeZone,
}),
});
const json = await r.json();
if (r.ok) {
alert("Booked! Check your email for confirmation.");
// json.bookingUid, calendar links available via /v2/bookings/{uid}/calendar-links
} else {
console.error(json);
alert("Booking failed.");
}
}
}
The /v2/slots response returns available times; you render them, then POST /v2/bookings with the chosen start time. Both endpoints require the cal-api-version header and auth. (Cal)