Skip to content

Calendars API

Manages calendar connections for availability checking and event creation.

Base Path: /api/calendars

Authentication: All endpoints require Bearer token.

Endpoints

MethodPathAuthDescription
GET/YesList calendar connections
GET/:idYesGet single calendar connection
GET/:id/listYesList calendars in a connection
PUT/:id/preferencesYesUpdate calendar preferences
DELETE/:idYesDisconnect calendar account

List Calendar Connections

Retrieves all calendar accounts connected to the user, ordered by creation date (oldest first).

GET /api/calendars

Response:

json
{
	"success": true,
	"data": [
		{
			"id": "uuid",
			"provider": "google",
			"provider_account_email": "[email protected]",
			"calendars_for_availability": ["primary", "[email protected]"],
			"calendar_for_events": "primary",
			"is_valid": true,
			"error_message": null,
			"created_at": "2025-01-07T10:00:00Z",
			"updated_at": "2025-01-07T10:00:00Z"
		}
	]
}

Note: Token values are never returned in API responses. The refresh_token is excluded.

Get Single Calendar Connection

Retrieves details for a specific calendar connection.

GET /api/calendars/:id

Response:

json
{
	"success": true,
	"data": {
		"id": "uuid",
		"user_id": "uuid",
		"provider": "google",
		"provider_account_id": "google-user-id",
		"provider_account_email": "[email protected]",
		"calendars_for_availability": ["primary"],
		"calendar_for_events": "primary",
		"is_valid": true,
		"error_message": null,
		"last_validated_at": "2025-01-07T10:00:00Z",
		"created_at": "2025-01-07T10:00:00Z",
		"updated_at": "2025-01-07T10:00:00Z"
	}
}

Note: The refresh_token field is explicitly removed from the response.

List Calendars in Connection

Retrieves all calendars available in a connected Google account.

GET /api/calendars/:id/list

Response:

json
{
	"success": true,
	"data": [
		{
			"id": "primary",
			"summary": "John Host",
			"description": null,
			"timeZone": "America/New_York",
			"backgroundColor": "#9fe1e7",
			"foregroundColor": "#000000",
			"accessRole": "owner",
			"primary": true,
			"selectedForAvailability": true,
			"selectedForEvents": true
		},
		{
			"id": "[email protected]",
			"summary": "Work Calendar",
			"description": "Team meetings and deadlines",
			"timeZone": "America/New_York",
			"backgroundColor": "#16a765",
			"foregroundColor": "#ffffff",
			"accessRole": "writer",
			"primary": false,
			"selectedForAvailability": true,
			"selectedForEvents": false
		}
	]
}

Side Effects:

  • Refreshes OAuth token if expired
  • Queries Google Calendar API for calendar list
  • Marks connection as is_valid: false if token refresh fails

Error Responses:

  • 400 - Connection is invalid (needs reconnection)
  • 404 - Connection not found
  • 500 - Google OAuth not configured

Update Calendar Preferences

Updates which calendars to check for conflicts and which to write events to.

PUT /api/calendars/:id/preferences

Request Body (Zod validated):

json
{
	"calendars_for_availability": ["primary", "[email protected]"],
	"calendar_for_events": "primary"
}

Fields:

FieldTypeDescription
calendars_for_availabilitystring[]Calendar IDs to check for conflicts
calendar_for_eventsstring | nullCalendar ID to create events in

Response:

json
{
	"success": true,
	"data": {
		"id": "uuid",
		"provider": "google",
		"provider_account_email": "[email protected]",
		"calendars_for_availability": ["primary", "[email protected]"],
		"calendar_for_events": "primary",
		"is_valid": true,
		"updated_at": "2025-01-07T11:00:00Z"
	}
}

Disconnect Calendar

Removes a calendar connection and revokes OAuth tokens.

DELETE /api/calendars/:id

Response:

json
{
	"success": true,
	"message": "Calendar disconnected"
}

Side Effects:

  • Attempts to revoke Google OAuth tokens (continues even if revocation fails)
  • Deletes connection record from database
  • Does not affect existing calendar events

Error Responses:

  • 401 - Unauthorized (missing or invalid token)
  • 404 - Connection not found or belongs to different user
  • 500 - Database deletion failed

Data Model

Database Table: calendar_connections

typescript
interface CalendarConnection {
	id: string; // UUID primary key
	user_id: string; // References auth.users(id)
	provider: 'google'; // Currently only Google supported
	provider_account_id: string; // Google account ID
	provider_account_email: string; // Display email
	refresh_token: string; // Never exposed in API responses
	calendars_for_availability: string[]; // JSONB - Calendar IDs for conflict checking
	calendar_for_events: string | null; // Calendar ID for event creation
	is_valid: boolean; // False if token refresh fails
	error_message: string | null; // Error details if invalid
	last_validated_at: string; // Last successful token use
	created_at: string;
	updated_at: string;
}

Column Notes

ColumnPurpose
calendars_for_availabilityJSONB array of calendar IDs to check during availability calculations
calendar_for_eventsSingle calendar ID where confirmed bookings create events
is_validSet to false when token refresh fails; user must reconnect
error_messageHuman-readable error message when is_valid is false

Calendar Selection

Calendars for Availability (Read)

These calendars are checked when calculating availability:

typescript
// During availability check
const busyTimes = await getBusyTimes(
	accessToken,
	connection.calendars_for_availability, // Check all selected
	startTime,
	endTime
);
  • Recommendation: Select all calendars that might contain conflicts
  • Example: Personal + Work + Shared team calendars

Calendar for Events (Write)

Events are created in this calendar when bookings are confirmed:

typescript
// During booking confirmation
await createCalendarEvent(
	accessToken,
	connection.calendar_for_events, // Write to this calendar
	eventDetails
);
  • Recommendation: Use the primary or most relevant calendar
  • Note: Only one write calendar per connection

Token Refresh Flow

OAuth tokens are automatically refreshed when performing calendar operations:

typescript
// src/api/routes/calendars/index.ts
async function getAccessToken(connection, supabase, clientId, clientSecret) {
	try {
		const { access_token } = await refreshAccessToken(
			connection.refresh_token,
			clientId,
			clientSecret
		);
		return access_token;
	} catch (error) {
		// Mark connection as invalid if refresh fails
		await supabase
			.from('calendar_connections')
			.update({
				is_valid: false,
				error_message: 'Failed to refresh access token. Please reconnect.'
			})
			.eq('id', connection.id);
		throw new Error('Calendar connection is no longer valid. Please reconnect.');
	}
}

Multiple Connections

Users can connect multiple Google accounts:

json
{
	"success": true,
	"data": [
		{
			"id": "uuid-1",
			"provider_account_email": "[email protected]",
			"created_at": "2025-01-05T10:00:00Z"
		},
		{
			"id": "uuid-2",
			"provider_account_email": "[email protected]",
			"created_at": "2025-01-07T10:00:00Z"
		}
	]
}

Behavior:

  • All connections' selected calendars are checked for availability conflicts
  • Events are created on the first connection (by created_at ascending) that has a calendar_for_events set
  • Connections are returned ordered by creation date (oldest first)

TypeScript Client Usage

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

const client = createApiClient(fetch);
const headers = { Authorization: `Bearer ${token}` };

// List connections
const connectionsRes = await client.api.calendars.$get({}, { headers });

// Get single connection
const connectionRes = await client.api.calendars[':id'].$get(
	{ param: { id: 'connection-uuid' } },
	{ headers }
);

// List calendars in connection
const calendarsRes = await client.api.calendars[':id'].list.$get(
	{ param: { id: 'connection-uuid' } },
	{ headers }
);

// Update preferences
const updateRes = await client.api.calendars[':id'].preferences.$put(
	{
		param: { id: 'connection-uuid' },
		json: {
			calendars_for_availability: ['primary', '[email protected]'],
			calendar_for_events: 'primary'
		}
	},
	{ headers }
);

// Disconnect
const deleteRes = await client.api.calendars[':id'].$delete(
	{ param: { id: 'connection-uuid' } },
	{ headers }
);

Internal documentation - Not for public distribution