Skip to content

Availability API

Calculates available time slots based on booking link configuration, existing bookings, and calendar busy times.

Base Path: /api/availability

Authentication: Public endpoints (used by guest booking portal).

Endpoints

MethodPathAuthDescription
GET/NoGet available slots for a date
GET/monthNoGet available dates in a month

Get Available Slots

Returns available time slots for a specific date.

GET /api/availability?bookingLinkId={id}&date={date}&timezone={tz}

Query Parameters (Zod validated):

ParameterTypeRequiredDefaultDescription
bookingLinkIdUUIDYes-Booking link to check
dateYYYY-MM-DDYes-Date to check availability
timezonestringNoUTCGuest's timezone (IANA format)

Success Response:

json
{
	"success": true,
	"data": {
		"date": "2025-01-15",
		"timezone": "America/New_York",
		"bookingLinkTimezone": "America/Los_Angeles",
		"bookingLink": {
			"id": "uuid",
			"name": "Guest Interview",
			"duration_minutes": 60,
			"description": "Standard interview slot",
			"timezone": "America/Los_Angeles"
		},
		"podcast": {
			"id": "uuid",
			"slug": "my-podcast",
			"title": "My Podcast"
		},
		"slots": [
			{
				"start": "2025-01-15T14:00:00.000Z",
				"end": "2025-01-15T15:00:00.000Z",
				"available": true
			},
			{
				"start": "2025-01-15T16:15:00.000Z",
				"end": "2025-01-15T17:15:00.000Z",
				"available": true
			}
		]
	}
}

Empty Slots Response (date constraints or no rules):

json
{
	"success": true,
	"data": {
		"date": "2025-01-15",
		"slots": [],
		"message": "Bookings require 24 hours notice"
	}
}

Slot Calculation Process

Filtering Steps

  1. Date Constraints: Validate against min_notice_hours and max_advance_days
  2. Day of Week Rules: Check if availability_rules exist for this day
  3. Generate Slots: Create potential slots based on duration and time windows
  4. Past Time Filter: Remove slots that have already passed
  5. Min Notice Filter: Remove slots within min_notice_hours of now
  6. Calendar Conflicts: Remove slots overlapping Google Calendar busy periods (with buffers)
  7. Booking Conflicts: Remove slots overlapping existing pending or confirmed bookings (with buffers)

Get Available Dates

Returns dates with availability in a given month (for calendar view).

GET /api/availability/month?bookingLinkId={id}&year={year}&month={month}&timezone={tz}

Query Parameters (Zod validated):

ParameterTypeRequiredDefaultDescription
bookingLinkIdUUIDYes-Booking link to check
yearYYYYYes-Year (e.g., 2025)
month1-12Yes-Month (1-12)
timezonestringNoUTCGuest's timezone (IANA format)

Response:

json
{
	"success": true,
	"data": {
		"year": 2025,
		"month": 1,
		"timezone": "America/New_York",
		"bookingLinkTimezone": "America/Los_Angeles",
		"bookingLink": {
			"id": "uuid",
			"name": "Guest Interview",
			"duration_minutes": 60,
			"timezone": "America/Los_Angeles"
		},
		"availableDates": [
			{ "date": "2025-01-13", "slots": 1 },
			{ "date": "2025-01-14", "slots": 1 },
			{ "date": "2025-01-15", "slots": 1 },
			{ "date": "2025-01-20", "slots": 1 }
		]
	}
}

Note: The slots count is a placeholder (always 1). The month endpoint performs a lightweight check based on availability rules only, without querying calendars. Actual slot counts are computed when the user selects a specific date.

Timezone Handling

The availability API handles timezone conversions between three contexts:

  1. Guest Timezone: Where the guest is located (from timezone query param)
  2. Host Timezone: The booking link's configured timezone (booking_links.timezone)
  3. UTC: Storage and calendar API communication

Critical Implementation Details

Creating Dates in Timezone

typescript
// src/api/routes/availability/index.ts
function createDateInTimezone(dateStr: string, timeStr: string, timezone: string): Date {
	// Uses Intl.DateTimeFormat to calculate UTC offset for target timezone
	// Handles DST transitions correctly
	// Returns Date object representing the correct UTC instant
}

Day of Week in Timezone

typescript
function getDayOfWeekInTimezone(dateStr: string, timezone: string): number {
	// Important: "2024-01-15" might be Monday in New York but Tuesday in Tokyo
	// Uses booking link's timezone to determine correct day of week
	// Returns 0=Sunday through 6=Saturday
}

Example Scenario

  • Booking Link: Configured for America/Los_Angeles (PST, UTC-8)
  • Availability Rule: Monday 9:00-17:00 (in host's timezone)
  • Guest: In America/New_York (EST, UTC-5)

When guest requests availability for Monday:

  • Host's 9:00 PST = 12:00 EST for guest display
  • Slots are generated in host's timezone, stored as UTC
  • Guest receives UTC ISO strings to convert locally

Availability Rules

Availability rules define when a host is available for bookings.

Rule Structure

typescript
// src/lib/types/booking.types.ts
interface AvailabilityRule {
	dayOfWeek: number; // 0=Sunday, 1=Monday, ..., 6=Saturday
	startTime: string; // "09:00" (24-hour format, HH:MM)
	endTime: string; // "17:00" (24-hour format, HH:MM)
}

Multiple Rules Per Day

A booking link can have multiple availability windows per day:

json
{
	"availability_rules": [
		{ "dayOfWeek": 1, "startTime": "09:00", "endTime": "12:00" },
		{ "dayOfWeek": 1, "startTime": "14:00", "endTime": "17:00" }
	]
}

This creates morning and afternoon slots on Mondays, with a lunch break.

Blocked Days

Days without rules are completely blocked. If no rules exist for Wednesday (day 3), no slots will be available on Wednesdays.

Conflict Detection

Google Calendar Integration

When checking availability, the API:

  1. Fetches host's calendar connections via podcast_members (owner/admin roles)
  2. Refreshes OAuth tokens if needed
  3. Queries Google Calendar FreeBusy API for each connection's calendars_for_availability
  4. Merges busy periods from all calendars
  5. Excludes busy periods from available slots
typescript
// Fetch busy times for selected calendars
const busyPeriods = await getBusyTimes(
	accessToken,
	connection.calendars_for_availability,
	startOfDay,
	endOfDay
);

Existing Bookings

Confirmed and pending bookings for the same booking link are excluded:

typescript
// Query existing bookings
const { data: existingBookings } = await supabase
	.from('bookings')
	.select('start_time, end_time')
	.eq('booking_link_id', bookingLinkId)
	.in('status', ['pending', 'confirmed'])
	.gte('start_time', `${date}T00:00:00Z`)
	.lte('start_time', `${date}T23:59:59Z`);

Buffer Time

Buffers ensure time between bookings for preparation:

  • buffer_before_minutes: Minutes before the slot that must be free
  • buffer_after_minutes: Minutes after the slot that must be free

Example: 60-minute slot at 10:00 with 15-minute buffers:

  • Effective block: 9:45 - 11:15
  • Next available slot: 11:15 or later
typescript
// Slot generation includes buffer spacing
slotStart = new Date(slotStart.getTime() + (durationMinutes + bufferAfter + bufferBefore) * 60000);

Constraints

Minimum Notice

min_notice_hours prevents last-minute bookings:

typescript
const minNoticeDate = new Date(now.getTime() + (bookingLink.min_notice_hours || 24) * 60 * 60000);

// Skip slots before min notice time
if (slot.start < minNoticeDate) {
	continue;
}

Maximum Advance

max_advance_days limits how far in advance guests can book:

typescript
const maxAdvanceDate = new Date(
	now.getTime() + (bookingLink.max_advance_days || 60) * 24 * 60 * 60000
);

// Date must be within max advance window
if (requestedDate > maxAdvanceDate) {
	return { slots: [], message: '...' };
}

Error Responses

CodeErrorCause
400Validation errorInvalid UUID, date format, or timezone
404Booking link not found or inactiveID doesn't exist or is_active: false
500Server configuration errorMissing Supabase credentials
500No host found for this podcastNo owner/admin in podcast_members
500Failed to fetch availabilityGeneral error (calendar API, database)

TypeScript Client Usage

typescript
import { createApiClient } from '$api/client';

const client = createApiClient(fetch);

// Get slots for a date (no auth required)
const slotsRes = await client.api.availability.$get({
	query: {
		bookingLinkId: 'uuid',
		date: '2025-01-15',
		timezone: 'America/New_York'
	}
});

// Get available dates in month
const monthRes = await client.api.availability.month.$get({
	query: {
		bookingLinkId: 'uuid',
		year: '2025',
		month: '1',
		timezone: 'America/New_York'
	}
});

Performance Considerations

  • Month View: Uses simplified check (availability rules only) - no calendar API calls
  • Day View: Full check including calendar API calls and booking queries
  • Calendar Token Refresh: Automatic, adds ~200ms latency when needed
  • No Caching: Calendar busy times are fetched fresh per request
  • Multiple Connections: All host calendar connections are checked (parallel token refresh)

Data Flow Diagram

Internal documentation - Not for public distribution