Skip to content

Bookings API

Manages the complete booking lifecycle from creation through confirmation/cancellation.

Base Path: /api/bookingsSource: src/api/routes/bookings/index.ts

Endpoints

MethodPathAuthDescription
POST/NoCreate new booking (public)
GET/YesList bookings for podcast
GET/:idYesGet booking details
POST/:id/confirmYesConfirm pending booking
POST/:id/cancelYesCancel confirmed booking
POST/:id/declineYesDecline pending booking
POST/:id/rescheduleYesMove the session to a new time (direct move via reschedule_booking_solo; all attendees emailed; no guest-acceptance step)
POST/:id/statusYesStamp an outcome on a past booking (completed / no-show / clear)
DELETE/:idYesHard delete booking

See Bookings Hub for the management UI over these endpoints, and Booking Sessions API & Confirm Path for the confirm path's side-effect sequence and the calendar-recovery endpoints.

Related Documentation

For a comprehensive guide connecting bookings, emails, and calendar events, see the Booking Flow Guide.

Create Booking (Public)

Creates a new booking request. This is a public endpoint used by the guest booking portal at book.podcasterplus.com.

POST /api/bookings

Request Body:

json
{
	"booking_link_id": "uuid",
	"start_time": "2025-01-15T10:00:00Z",
	"end_time": "2025-01-15T11:00:00Z",
	"timezone": "America/New_York",
	"guest_name": "John Doe",
	"guest_email": "[email protected]",
	"guest_phone": "+1234567890",
	"custom_field_responses": {
		"topic": "AI in podcasting",
		"bio": "Tech entrepreneur..."
	},
	"guest_notes": "Looking forward to the conversation"
}

Response 201 Created:

json
{
	"success": true,
	"data": {
		"id": "uuid",
		"start_time": "2025-01-15T10:00:00Z",
		"end_time": "2025-01-15T11:00:00Z",
		"status": "pending",
		"podcast": {
			"id": "uuid",
			"slug": "my-podcast",
			"title": "My Podcast"
		},
		"booking_link": {
			"name": "Guest Interview",
			"duration_minutes": 60
		}
	}
}

Side Effects (in order):

  1. Validates booking link exists and is active
  2. Validates time slot (minimum notice, maximum advance, no conflicts)
  3. Creates booking record with status pending
  4. Increments total_bookings counter on booking link
  5. Creates episode record with status pending_confirmation
  6. Creates episode_guests record with 32-character access token (status: invited)
  7. Links booking to episode and guest records
  8. Sends booking-request email to guest
  9. Sends host-notification email to podcast owner (via podcast_members role lookup)

Error Responses:

  • 400 - Invalid input, missing required fields, or time validation failed
  • 404 - Booking link not found or inactive
  • 409 - Time slot already booked or unavailable

List Bookings

Retrieves all bookings for a podcast. Requires admin+ (#293 §15.5: the bookings board is Host/Producer work; a rostered Co-host reads booking info through the episode surface, where RLS scopes it).

GET /api/bookings?podcastId={id}

Query Parameter Name

The query parameter is podcastId (camelCase), not podcast_id.

Query Parameters:

ParameterTypeRequiredDescription
podcastIdUUIDYesPodcast to list bookings for
statusstringNoFilter by status (pending, confirmed, canceled, completed)
from_dateISO 8601NoStart of date range
to_dateISO 8601NoEnd of date range
limitnumberNoMax results (default: 50)
offsetnumberNoPagination offset

Response:

json
{
	"success": true,
	"data": [
		{
			"id": "uuid",
			"booking_link_id": "uuid",
			"podcast_id": "uuid",
			"episode_id": "uuid",
			"status": "confirmed",
			"start_time": "2025-01-15T10:00:00Z",
			"end_time": "2025-01-15T11:00:00Z",
			"timezone": "America/New_York",
			"guest_name": "John Doe",
			"guest_email": "[email protected]",
			"guest_phone": "+1234567890",
			"meeting_url": "https://meet.google.com/abc-xyz",
			"google_event_id": "google_event_id",
			"google_calendar_id": "primary",
			"created_at": "2025-01-07T10:00:00Z",
			"confirmed_at": "2025-01-07T12:00:00Z",
			"booking_links": {
				"name": "Guest Interview",
				"slug": "interview",
				"duration_minutes": 60
			}
		}
	]
}

Get Booking

Retrieves a single booking with full details. Requires admin+ (#293 §15.5).

GET /api/bookings/:id

Response:

json
{
	"success": true,
	"data": {
		"id": "uuid",
		"booking_link_id": "uuid",
		"podcast_id": "uuid",
		"episode_id": "uuid",
		"guest_id": "uuid",
		"status": "confirmed",
		"start_time": "2025-01-15T10:00:00Z",
		"end_time": "2025-01-15T11:00:00Z",
		"timezone": "America/New_York",
		"guest_name": "John Doe",
		"guest_email": "[email protected]",
		"guest_phone": "+1234567890",
		"guest_notes": "Looking forward to the conversation",
		"custom_field_responses": {
			"topic": "AI in podcasting",
			"bio": "Tech entrepreneur..."
		},
		"meeting_url": "https://meet.google.com/abc-xyz",
		"google_event_id": "google_event_id",
		"google_calendar_id": "primary",
		"created_at": "2025-01-07T10:00:00Z",
		"confirmed_at": "2025-01-07T12:00:00Z",
		"booking_links": {
			"name": "Guest Interview",
			"slug": "interview",
			"duration_minutes": 60,
			"confirmation_message": "Thanks for booking!"
		},
		"podcasts": {
			"id": "uuid",
			"slug": "my-podcast",
			"title": "My Podcast"
		}
	}
}

Confirm Booking

Confirms a pending booking. Requires owner or admin role on the podcast.

POST /api/bookings/:id/confirm

Request Body: None required. Calendar events are created automatically if the host has a connected Google Calendar.

Response 200 OK:

json
{
	"success": true,
	"data": {
		"id": "uuid",
		"status": "confirmed",
		"meeting_url": "https://meet.google.com/abc-xyz",
		"google_event_id": "google_event_id",
		"google_calendar_id": "primary",
		"confirmed_at": "2025-01-07T12:00:00Z",
		"confirmed_by": "user-uuid",
		"google_calendar_created": true
	}
}

Side Effects (in order):

  1. Verifies user has owner or admin role on podcast
  2. Verifies booking status is pending
  3. Creates Google Calendar event (if host has connected calendar):
    • Summary: "{Podcast Title}: Recording with {Guest Name}"
    • Description: Booking link name + guest contact info
    • Attendees: Guest email with responseStatus: 'needsAction' (sends Google invite)
    • Reminders: 24-hour email + 30-minute popup
    • A Google Meet conference ONLY when the session's effective recording platform is google_meet (#200); every other platform sets the event's location to the room URL and adds a "Join the recording" description line, because the Calendar API accepts conferenceData.createRequest for Meet and add-on solutions only
  4. Updates booking:
    • statusconfirmed
    • confirmed_at → current timestamp
    • confirmed_by → user ID
    • booking_sessions.meeting_url → the Meet room (if created) or the frozen manual link; booking_sessions.meeting_platform → the effective platform
    • google_event_id → Google event ID
    • google_calendar_id → Calendar ID used
  5. Updates episode: statusdraft
  6. Updates episode_guest: statusactive
  7. Sends booking-confirmed email to guest (includes meeting URL if available)
  8. Emits booking.confirmed automation event
  9. Schedules time-based automation jobs (time.before_recording, time.after_recording, time.after_booking)

Cancel Booking

Cancels a confirmed or pending booking. Requires owner or admin role.

POST /api/bookings/:id/cancel

Request Body (optional):

json
{
	"reason": "Schedule conflict"
}

Response 200 OK:

json
{
	"success": true,
	"data": {
		"id": "uuid",
		"status": "canceled",
		"canceled_at": "2025-01-08T10:00:00Z",
		"canceled_by": "host",
		"cancellation_reason": "Schedule conflict"
	}
}

Side Effects (in order):

  1. Verifies booking can be canceled (not already canceled/completed)
  2. Deletes Google Calendar event (if exists) with sendUpdates: 'all' (notifies attendees)
  3. Updates booking:
    • statuscanceled
    • canceled_at → current timestamp
    • canceled_by'host'
    • cancellation_reason → provided reason
  4. Updates episode: statusdraft
  5. Updates episode_guest: statusexpired
  6. Emits booking.canceled automation event
  7. Cancels pending time-based automation jobs for this booking

Decline Booking

Declines a pending booking request. Can only be used on bookings with pending status (not confirmed). Requires owner or admin role.

POST /api/bookings/:id/decline

Request Body (optional):

json
{
	"reason": "Not a good fit for the show"
}

Response 200 OK:

json
{
	"success": true,
	"data": {
		"id": "uuid",
		"status": "canceled",
		"canceled_at": "2025-01-07T14:00:00Z",
		"canceled_by": "host",
		"cancellation_reason": "Not a good fit for the show"
	},
	"message": "Booking declined and guest notified"
}

Status Note

Decline sets booking status to canceled (not declined). The cancellation_reason field stores the decline reason.

Side Effects (in order):

  1. Verifies booking status is pending (cannot decline confirmed bookings - use cancel instead)
  2. Updates booking:
    • statuscanceled
    • canceled_at → current timestamp
    • cancellation_reason → provided reason
  3. Updates episode: statusdraft
  4. Updates episode_guest: statusexpired
  5. Sends booking-declined email to guest (includes reason if provided)
  6. Emits booking.declined automation event

Delete Booking

Hard deletes a booking. Requires owner or admin role. Use with caution.

DELETE /api/bookings/:id

Status Restriction

Only bookings with status pending or canceled can be deleted. Confirmed or completed bookings must be canceled first.

Response:

json
{
	"success": true,
	"message": "Booking deleted successfully"
}

Side Effects:

  • Deletes episode_guest record (if exists)
  • Deletes associated episode (only if episode was created by booking and is in pending_confirmation or draft status)
  • Deletes booking record

Error Response for status restriction:

json
{
	"error": "Cannot delete booking with status: confirmed. Only pending or canceled bookings can be deleted."
}

Booking Status Flow

Status Values

The database uses canceled (American spelling) not cancelled. Both decline and cancel operations result in canceled status.

Data Model

typescript
interface Booking {
	id: string;
	booking_link_id: string;
	podcast_id: string;
	episode_id: string | null;
	guest_id: string | null; // Links to episode_guests
	status: 'pending' | 'confirmed' | 'canceled' | 'completed';
	start_time: string; // ISO 8601
	end_time: string; // ISO 8601
	timezone: string; // IANA timezone
	guest_name: string;
	guest_email: string;
	guest_phone: string | null;
	guest_notes: string | null;
	custom_field_responses: Record<string, unknown> | null; // JSON blob

	// Calendar integration
	meeting_url: string | null; // the recording link, whatever the platform (#200)
	google_event_id: string | null; // Google Calendar event ID
	google_calendar_id: string | null; // Calendar the event was created in

	// Timestamps
	created_at: string;
	confirmed_at: string | null;
	confirmed_by: string | null; // User ID who confirmed
	canceled_at: string | null;
	canceled_by: 'host' | 'guest' | null;
	cancellation_reason: string | null;
}

episode_guests (linked via guest_id):

typescript
interface EpisodeGuest {
	id: string;
	episode_id: string;
	user_id: string | null;
	email: string;
	name: string;
	status: 'invited' | 'active' | 'completed' | 'expired';
	access_token: string | null; // 32-char token for magic link portal access
	invited_at: string;
	last_accessed_at: string | null;
	notes: string | null; // Stores custom field responses as JSON
}

TypeScript Client Usage

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

const client = createApiClient(fetch);

// Create booking (public - no auth)
const createRes = await client.api.bookings.$post({
	json: {
		booking_link_id: 'uuid',
		start_time: '2025-01-15T10:00:00Z',
		end_time: '2025-01-15T11:00:00Z',
		timezone: 'America/New_York',
		guest_name: 'John Doe',
		guest_email: '[email protected]'
	}
});

// List bookings (requires auth)
const listRes = await client.api.bookings.$get(
	{ query: { podcast_id: 'uuid', status: 'pending' } },
	{ headers: { Authorization: `Bearer ${token}` } }
);

// Confirm booking
const confirmRes = await client.api.bookings[':id'].confirm.$post(
	{ param: { id: 'booking-uuid' } },
	{ headers: { Authorization: `Bearer ${token}` } }
);

Internal documentation - Not for public distribution