Bookings API
Manages the complete booking lifecycle from creation through confirmation/cancellation.
Base Path: /api/bookingsSource: src/api/routes/bookings/index.ts
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST | / | No | Create new booking (public) |
GET | / | Yes | List bookings for podcast |
GET | /:id | Yes | Get booking details |
POST | /:id/confirm | Yes | Confirm pending booking |
POST | /:id/cancel | Yes | Cancel confirmed booking |
POST | /:id/decline | Yes | Decline pending booking |
POST | /:id/reschedule | Yes | Move the session to a new time (direct move via reschedule_booking_solo; all attendees emailed; no guest-acceptance step) |
POST | /:id/status | Yes | Stamp an outcome on a past booking (completed / no-show / clear) |
DELETE | /:id | Yes | Hard 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/bookingsRequest Body:
{
"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:
{
"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):
- Validates booking link exists and is active
- Validates time slot (minimum notice, maximum advance, no conflicts)
- Creates
bookingrecord with statuspending - Increments
total_bookingscounter on booking link - Creates
episoderecord with statuspending_confirmation - Creates
episode_guestsrecord with 32-character access token (status:invited) - Links booking to episode and guest records
- Sends
booking-requestemail to guest - Sends
host-notificationemail to podcast owner (viapodcast_membersrole lookup)
Error Responses:
400- Invalid input, missing required fields, or time validation failed404- Booking link not found or inactive409- 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
podcastId | UUID | Yes | Podcast to list bookings for |
status | string | No | Filter by status (pending, confirmed, canceled, completed) |
from_date | ISO 8601 | No | Start of date range |
to_date | ISO 8601 | No | End of date range |
limit | number | No | Max results (default: 50) |
offset | number | No | Pagination offset |
Response:
{
"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/:idResponse:
{
"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/confirmRequest Body: None required. Calendar events are created automatically if the host has a connected Google Calendar.
Response 200 OK:
{
"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):
- Verifies user has
owneroradminrole on podcast - Verifies booking status is
pending - 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'slocationto the room URL and adds a "Join the recording" description line, because the Calendar API acceptsconferenceData.createRequestfor Meet and add-on solutions only
- Summary:
- Updates booking:
status→confirmedconfirmed_at→ current timestampconfirmed_by→ user IDbooking_sessions.meeting_url→ the Meet room (if created) or the frozen manual link;booking_sessions.meeting_platform→ the effective platformgoogle_event_id→ Google event IDgoogle_calendar_id→ Calendar ID used
- Updates episode:
status→draft - Updates episode_guest:
status→active - Sends
booking-confirmedemail to guest (includes meeting URL if available) - Emits
booking.confirmedautomation event - 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/cancelRequest Body (optional):
{
"reason": "Schedule conflict"
}Response 200 OK:
{
"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):
- Verifies booking can be canceled (not already canceled/completed)
- Deletes Google Calendar event (if exists) with
sendUpdates: 'all'(notifies attendees) - Updates booking:
status→canceledcanceled_at→ current timestampcanceled_by→'host'cancellation_reason→ provided reason
- Updates episode:
status→draft - Updates episode_guest:
status→expired - Emits
booking.canceledautomation event - 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/declineRequest Body (optional):
{
"reason": "Not a good fit for the show"
}Response 200 OK:
{
"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):
- Verifies booking status is
pending(cannot decline confirmed bookings - use cancel instead) - Updates booking:
status→canceledcanceled_at→ current timestampcancellation_reason→ provided reason
- Updates episode:
status→draft - Updates episode_guest:
status→expired - Sends
booking-declinedemail to guest (includes reason if provided) - Emits
booking.declinedautomation event
Delete Booking
Hard deletes a booking. Requires owner or admin role. Use with caution.
DELETE /api/bookings/:idStatus Restriction
Only bookings with status pending or canceled can be deleted. Confirmed or completed bookings must be canceled first.
Response:
{
"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_confirmationordraftstatus) - Deletes booking record
Error Response for status restriction:
{
"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
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;
}Related Tables
episode_guests (linked via guest_id):
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
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}` } }
);Related
- Booking Links API - Event type configuration
- Availability API - Time slot availability
- Google Calendar Integration - Calendar sync
- Automation Engine - Event triggers