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.

typescript
// Inside an authorized host query:
const resource = await ctx.runQuery(components.booking.resources.getResource, {
  id: "meeting-room",
});

Setup and availability

ModuleCommon operations
resourcescreateResource, getResource, listResources, updateResource, deleteResource
schedulescreateSchedule, getSchedule, getDefaultSchedule, getEffectiveAvailability, date overrides
publiccreateEventType, getEventType, getEventTypeBySlug, listEventTypes
resource_event_typeslinkResourceToEventType, 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:

typescript
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:

typescript
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

FunctionPurpose
public.createBookingBook one exclusive resource
public.createProvisionalBookingHold an exclusive resource until your host confirms or expires it
public.getBooking / getBookingByUidRead a booking from authorized host code
public.getBookingByTokenRead a booking using its UID and management token
public.cancelBookingByTokenCancel using UID and token
public.rescheduleBookingMove a booking using its component document ID
public.rescheduleBookingByTokenMove using UID and token
hooks.transitionBookingStateAdminister 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.

typescript
// 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:

typescript
// 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.

typescriptconvex/booking-internal.ts
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.

tsxexamples/slot-hold.tsx
"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.

tsxexamples/slots.tsx
"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.

tsxexamples/validation.tsx
"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.