API Reference
This page describes the component API. The reference gateway
adds host policy and sometimes uses different arguments. Your generated
components.booking types are the complete, version-matched reference.
Calling the component
Inside a Convex function, import components from ./_generated/api and call
ctx.runQuery or ctx.runMutation. Browser clients call your host wrappers.
Apply authorization before returning private data or
changing resources, and booking policy before creating
or moving a booking.
// Inside an authorized host query:
const resource = await ctx.runQuery(components.booking.resources.getResource, {
id: "meeting-room",
});Setup and availability
| Module | Common operations |
|---|---|
resources | createResource, getResource, listResources, updateResource, deleteResource |
schedules | createSchedule, getSchedule, getDefaultSchedule, getEffectiveAvailability, date overrides |
public | createEventType, getEventType, getEventTypeBySlug, listEventTypes |
resource_event_types | linkResourceToEventType, hasResourceEventTypeLink, link/unlink and lookup operations |
Resource and event-type id values are your app's strings. A booking's _id is a
component document ID; its uid is used in management links. They are not interchangeable.
Resources can carry metadata: Record<string, string>; updating that field replaces
the whole map, while omitting it preserves the existing map.
Month availability
public.getMonthAvailability returns a map of date strings to booleans. Pass the
resource's IANA timezone and a schedule ID for schedule-aware results:
const month = await ctx.runQuery(components.booking.public.getMonthAvailability, {
resourceId: "meeting-room",
dateFrom: "2027-03-01",
dateTo: "2027-03-31",
eventLength: 30,
slotInterval: 30,
resourceTimezone: "America/New_York",
scheduleId: "business-hours",
});Day slots
public.getDaySlots returns Array<{ time: string }> with ISO timestamp strings.
Unlike the month query, it takes the day's available slot indices, not a
schedule ID. The reference gateway resolves this for the Booker:
const availability = await ctx.runQuery(
components.booking.schedules.getEffectiveAvailability,
{ scheduleId: "business-hours", date: "2027-03-02" }
);
const slots = await ctx.runQuery(components.booking.public.getDaySlots, {
resourceId: "meeting-room",
date: "2027-03-02",
eventLength: 30,
slotInterval: 30,
resourceTimezone: "America/New_York",
availableSlots: availability.availableSlots,
});The slot indices represent local 15-minute buckets, from 0 to 95. An empty array means closed. Without schedule information, these queries fall back to 09:00–17:00 UTC; pass real schedule information for your application.
Both queries accept excludeBookingUid when displaying a reschedule calendar.
This excludes only that booking's exclusive-resource occupancy. For a resource
pool, use multi_resource.checkMultiResourceAvailability instead.
Bookings
| Function | Purpose |
|---|---|
public.createBooking | Book one exclusive resource |
public.createProvisionalBooking | Hold an exclusive resource until your host confirms or expires it |
public.getBooking / getBookingByUid | Read a booking from authorized host code |
public.getBookingByToken | Read a booking using its UID and management token |
public.cancelBookingByToken | Cancel using UID and token |
public.rescheduleBooking | Move a booking using its component document ID |
public.rescheduleBookingByToken | Move using UID and token |
hooks.transitionBookingState | Administer lifecycle transitions |
Creation takes epoch milliseconds for start and end, a timezone, event type,
resource, booker details and location. It returns the booking document.
Provisional bookings do not expire automatically. Have your host schedule a call
to public.expireProvisionalBooking when its confirmation window ends.
// Inside your mutation, AFTER authorization and host-policy validation:
const booking = await ctx.runMutation(components.booking.public.createBooking, {
eventTypeId: "quick-meeting",
resourceId: "meeting-room",
start: Date.parse("2027-03-02T15:00:00.000Z"),
end: Date.parse("2027-03-02T15:30:00.000Z"),
timezone: "America/New_York",
booker: { name: "Ada", email: "ada@example.com" },
location: { type: "inPerson", value: "Meeting Room" },
});Cancellation releases all resources held by an active booking. A repeated or invalid terminal transition does not release inventory again. Completed bookings retain historical occupancy.
Both reschedule entry points move all booking items atomically: a conflict leaves the original booking untouched. A successful move returns a new booking with a new UID, retaining its management token and resource quantities. Use the returned UID for subsequent management requests. Host wrappers must validate the new time against schedules, duration choices and notice periods too.
Multiple resources
Use multi_resource.createMultiResourceBooking for bundles or pooled resources:
// Inside a host mutation after policy checks:
const booking = await ctx.runMutation(
components.booking.multi_resource.createMultiResourceBooking,
{
eventTypeId: "studio-session",
resources: [
{ resourceId: "studio-a", quantity: 1 },
{ resourceId: "camera-pool", quantity: 2 },
],
start: Date.parse("2027-03-02T10:00:00.000Z"),
end: Date.parse("2027-03-02T11:00:00.000Z"),
timezone: "UTC",
booker: { name: "Ada", email: "ada@example.com" },
}
);resources must contain unique IDs and positive safe-integer quantities.
Exclusive resources accept quantity 1. Pools use isFungible: true and a positive
integer quantity as capacity. Ordinary/provisional single-resource booking APIs
do not accept pools; their slot queries do not offer pooled resources.
Use checkMultiResourceAvailability with the same resource requests and time
range for a preview, getBookingWithItems to read the bundle, and
cancelMultiResourceBooking for authorized administration. Creation always checks
inventory again, regardless of an earlier availability query.
Presence
presence.heartbeat takes { resourceId, slots: string[], user, eventTypeId? }.
Each slot is a full ISO timestamp for a 15-minute bucket. user is a client
session identifier, not proof of an authenticated identity.
The React useSlotHold hook maintains heartbeats and cleanup. Presence helps
visitors see one another's selections; it is not an exclusive inventory lock.
The final booking write can still reject a conflict.
Internal helpers
makeInternalBookingAPI creates server-only helpers under Convex's internal
namespace. It is optional; direct components.booking.* calls are sufficient.
import { makeInternalBookingAPI } from "@mrfinch/booking";
import { components } from "./_generated/api";
export const { getResource, getDaySlots } = makeInternalBookingAPI(components.booking);The old makeBookingAPI public factory was removed in 0.4.0. Migrate browser
endpoints to authorized host wrappers. Maintenance functions such as
maintenance.wipeAllData are destructive; keep reset wrappers internal.
Frontend Hooks
Import these hooks from @mrfinch/booking/react. For hooks that query or mutate,
render beneath your Convex provider, ConvexQueryCacheProvider and
BookingProvider, as shown in the Quick Start.
useSlotHold
useSlotHold(resourceId, slotId, durationMinutes = 60, eventTypeId?) returns the
session ID string, not a loading/error object. Pass null to stop holding a
selection. It sends heartbeats every five seconds and releases on cleanup.
Presence is best effort: it does not guarantee that a booking will succeed.
"use client";
import { useSlotHold } from "@mrfinch/booking/react";
export function SlotSelection({ selectedSlot }: { selectedSlot: string | null }) {
const sessionId = useSlotHold("meeting-room", selectedSlot, 30, "quick-meeting");
return <p data-session={sessionId}>{selectedSlot ? "Slot selected" : "Choose a slot"}</p>;
}slotId is a full ISO timestamp. Duration is in minutes; a five-hour selection
sends twenty 15-minute buckets regardless of the displayed slot interval.
useConvexSlots
The positional signature is
useConvexSlots(resourceId, eventLength, slotInterval?, allDurationOptions?, enabled = true, timezone?).
The default timezone is the browser's timezone. Call fetchSlots(date) to select
a day and fetchMonthSlots(date) to request the month's availability.
"use client";
import { useEffect } from "react";
import { useConvexSlots } from "@mrfinch/booking/react";
export function SlotList({ date }: { date: Date }) {
const { availableSlots, reservedSlots, isLoading, fetchSlots } =
useConvexSlots("meeting-room", 30, 30, undefined, true, "America/New_York");
useEffect(() => { fetchSlots(date); }, [date, fetchSlots]);
if (isLoading) return <p>Loading slots…</p>;
return (
<ul>
{availableSlots.map((slot) => <li key={slot.time}>{slot.time}</li>)}
{reservedSlots.map((slot) => <li key={slot.time}>{slot.time} — being selected</li>)}
</ul>
);
}Returns monthSlots, availableSlots, reservedSlots, isLoading,
fetchMonthSlots and fetchSlots. Slots have a time ISO string, not slot or
formatted properties. Reserved here means presence held by another session;
it does not mean a committed booking or server-enforced reservation.
useBookingValidation
useBookingValidation(eventType, resource, hasLink, selectedDuration, resourceId)
validates objects you already loaded. undefined means loading; null means a
missing record. It does not fetch them for you.
"use client";
import { useBookingValidation, type EventType, type Resource } from "@mrfinch/booking/react";
export function SelectionStatus(props: {
eventType: EventType | null | undefined;
resource: Resource | null | undefined;
hasLink: boolean | null | undefined;
}) {
const result = useBookingValidation(
props.eventType, props.resource, props.hasLink, 30, "meeting-room"
);
if (result.status === "loading") return <p>Loading…</p>;
if (result.status === "error") return <p role="alert">{result.error?.message}</p>;
return <p>Selection is valid.</p>;
}Returns { status: "loading" | "valid" | "error", error? }. The error object
contains type, message and recoveryPath; there is no data field.
This catches deleted/inactive setup, removed durations and unlinked resources in
the UI. The server must independently validate every write.