# Calendar Integrations — Google, Microsoft, Apple

This document describes how to integrate **Google Calendar**, **Microsoft Outlook / 365**, and **Apple iCloud Calendar** for conflict detection (free/busy) and event creation.

---

## Overview

```mermaid
flowchart LR
    subgraph App
        SYNC[Calendar Sync Service]
        CACHE[(Busy Cache)]
    end

    subgraph Providers
        G[Google Calendar API]
        M[Microsoft Graph]
        A[Apple CalDAV]
    end

    SYNC -->|OAuth 2.0| G
    SYNC -->|OAuth 2.0| M
    SYNC -->|App Password| A
    G --> CACHE
    M --> CACHE
    A --> CACHE
```

| Capability | Google | Microsoft | Apple iCloud |
|------------|--------|-----------|--------------|
| OAuth 2.0 | Yes | Yes | No (CalDAV + app password) |
| List calendars | Yes | Yes | Yes |
| Free/busy | Yes | Yes | Yes (VFREEBUSY) |
| Create events | Yes | Yes | Yes (VEVENT PUT) |
| Add attendee | Yes | Yes | Limited |
| Webhooks (push) | Yes (Channels) | Yes (Subscriptions) | No |
| Token refresh | Refresh token | Refresh token | Password-based |

---

## Google Calendar

### Prerequisites

1. Create a project in [Google Cloud Console](https://console.cloud.google.com).
2. Enable **Google Calendar API**.
3. Configure **OAuth consent screen** (External for production).
4. Create **OAuth 2.0 Client ID** (Web application).
5. Add authorized redirect URI:
   ```
   https://api.book.example.com/v1/oauth/callback/google
   ```

### OAuth Scopes

| Scope | Purpose |
|-------|---------|
| `https://www.googleapis.com/auth/calendar.readonly` | Read calendars and free/busy (minimum) |
| `https://www.googleapis.com/auth/calendar.events` | Create/update/delete events |
| `openid email profile` | Identify user |

**Recommended combined scope for full sync:**

```
https://www.googleapis.com/auth/calendar.events
```

(`calendar.events` includes read access to events on writable calendars.)

### OAuth Flow

1. **Authorize URL:**

```
GET https://accounts.google.com/o/oauth2/v2/auth
  ?client_id={CLIENT_ID}
  &redirect_uri={REDIRECT_URI}
  &response_type=code
  &scope=https://www.googleapis.com/auth/calendar.events openid email profile
  &access_type=offline
  &prompt=consent
  &state={signed_state_with_user_id}
```

2. **Token exchange:**

```http
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

code={code}&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&redirect_uri={REDIRECT_URI}&grant_type=authorization_code
```

3. Store `refresh_token` encrypted. Use it to refresh access tokens before expiry.

### List Calendars

```http
GET https://www.googleapis.com/calendar/v3/users/me/calendarList
Authorization: Bearer {access_token}
```

### Free/Busy Query

```http
POST https://www.googleapis.com/calendar/v3/freeBusy
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "timeMin": "2026-06-26T00:00:00Z",
  "timeMax": "2026-07-26T00:00:00Z",
  "timeZone": "UTC",
  "items": [{ "id": "primary" }, { "id": "abc@gmail.com" }]
}
```

**Response:** `calendars.{id}.busy[]` with `{ start, end }` intervals.

### Create Event

```http
POST https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "summary": "30 Minute Intro Call with Bob Jones",
  "description": "Booked via book.example.com\nGuest: bob@example.com",
  "start": { "dateTime": "2026-06-26T14:00:00", "timeZone": "America/New_York" },
  "end": { "dateTime": "2026-06-26T14:30:00", "timeZone": "America/New_York" },
  "attendees": [{ "email": "bob@example.com", "displayName": "Bob Jones" }],
  "reminders": { "useDefault": true }
}
```

Store returned `event.id` on the booking record for updates/cancellation.

### Delete Event

```http
DELETE https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events/{eventId}
Authorization: Bearer {access_token}
```

### Push Notifications (Optional)

Use [Calendar API push notifications](https://developers.google.com/calendar/api/guides/push) to invalidate busy cache when the host's calendar changes:

1. `POST /calendars/{id}/events/watch` with HTTPS webhook URL.
2. Channel expires (~7 days) — renew via cron job.

---

## Microsoft Outlook / Microsoft 365

### Prerequisites

1. Register app in [Azure Portal](https://portal.azure.com) → App registrations.
2. Add redirect URI:
   ```
   https://api.book.example.com/v1/oauth/callback/microsoft
   ```
3. Create client secret.
4. Add API permissions (Microsoft Graph):
   - `Calendars.Read`
   - `Calendars.ReadWrite`
   - `offline_access`
   - `openid`, `email`, `profile`

### OAuth Scopes

```
openid profile email offline_access Calendars.ReadWrite
```

### OAuth Flow

1. **Authorize URL:**

```
GET https://login.microsoftonline.com/common/oauth2/v2.0/authorize
  ?client_id={CLIENT_ID}
  &response_type=code
  &redirect_uri={REDIRECT_URI}
  &scope=openid profile email offline_access Calendars.ReadWrite
  &state={signed_state}
```

2. **Token exchange:**

```http
POST https://login.microsoftonline.com/common/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&code={code}&redirect_uri={REDIRECT_URI}&grant_type=authorization_code
```

### List Calendars

```http
GET https://graph.microsoft.com/v1.0/me/calendars
Authorization: Bearer {access_token}
```

### Free/Busy (Get Schedule)

Preferred for multi-calendar conflict check:

```http
POST https://graph.microsoft.com/v1.0/me/calendar/getSchedule
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "schedules": ["alice@outlook.com"],
  "startTime": { "dateTime": "2026-06-26T00:00:00", "timeZone": "UTC" },
  "endTime": { "dateTime": "2026-07-26T00:00:00", "timeZone": "UTC" },
  "availabilityViewInterval": 15
}
```

**Response:** `value[0].scheduleItems[]` with `{ start, end, status }`.

Alternative: fetch events via calendar view:

```http
GET https://graph.microsoft.com/v1.0/me/calendarView?startDateTime=2026-06-26T00:00:00Z&endDateTime=2026-07-26T00:00:00Z
Authorization: Bearer {access_token}
```

### Create Event

```http
POST https://graph.microsoft.com/v1.0/me/calendars/{calendarId}/events
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "subject": "30 Minute Intro Call with Bob Jones",
  "body": {
    "contentType": "text",
    "content": "Booked via book.example.com\nGuest: bob@example.com"
  },
  "start": { "dateTime": "2026-06-26T14:00:00", "timeZone": "America/New_York" },
  "end": { "dateTime": "2026-06-26T14:30:00", "timeZone": "America/New_York" },
  "attendees": [
    {
      "emailAddress": { "address": "bob@example.com", "name": "Bob Jones" },
      "type": "required"
    }
  ]
}
```

Store returned `id` on booking.

### Delete Event

```http
DELETE https://graph.microsoft.com/v1.0/me/events/{eventId}
Authorization: Bearer {access_token}
```

### Change Notifications (Optional)

Subscribe to calendar changes:

```http
POST https://graph.microsoft.com/v1.0/subscriptions
```

Renew before `expirationDateTime` (max ~3 days for calendar events).

---

## Apple iCloud Calendar

Apple does not offer a public REST API equivalent to Google/Microsoft for third-party apps. Integration uses **CalDAV** against iCloud servers.

### Prerequisites

1. Host must have an **Apple ID** with iCloud Calendar enabled.
2. Host generates an **app-specific password**:
   - [appleid.apple.com](https://appleid.apple.com) → Sign-In and Security → App-Specific Passwords
3. Store credentials encrypted (Apple ID email + app-specific password).

### CalDAV Endpoints

| Resource | URL |
|----------|-----|
| Principal discovery | `https://caldav.icloud.com/` |
| Calendars | `https://caldav.icloud.com/{dsid}/calendars/` |

Use `PROPFIND` to discover calendar home set and calendar collection URLs.

### Authentication

```http
Authorization: Basic base64({appleId}:{appSpecificPassword})
```

### List Calendars (PROPFIND)

```http
PROPFIND https://caldav.icloud.com/
Depth: 1
Authorization: Basic ...
Content-Type: application/xml

<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  <D:prop>
    <D:displayname />
    <C:calendar-description />
    <C:supported-calendar-component-set />
  </D:prop>
</D:propfind>
```

Filter collections where `supported-calendar-component-set` includes `VEVENT`.

### Free/Busy (VFREEBUSY Report)

```http
REPORT {calendarUrl}
Depth: 1
Authorization: Basic ...
Content-Type: application/xml

<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  <D:prop>
    <C:calendar-data />
  </D:prop>
  <C:filter>
    <C:comp-filter name="VEVENT">
      <C:time-range start="20260626T000000Z" end="20260726T000000Z"/>
    </C:comp-filter>
  </C:filter>
</C:calendar-query>
```

Parse returned `VEVENT` components for `DTSTART`/`DTEND` to build busy intervals.

Alternatively use `free-busy-set` property if exposed on the calendar collection.

### Create Event (PUT VEVENT)

```http
PUT {calendarUrl}/{uuid}.ics
Authorization: Basic ...
Content-Type: text/calendar; charset=utf-8
If-None-Match: *

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//book.example.com//EN
BEGIN:VEVENT
UID:{uuid}@book.example.com
DTSTAMP:20260625T120000Z
DTSTART;TZID=America/New_York:20260626T100000
DTEND;TZID=America/New_York:20260626T103000
SUMMARY:30 Minute Intro Call with Bob Jones
DESCRIPTION:Booked via book.example.com
ORGANIZER:mailto:alice@icloud.com
ATTENDEE:mailto:bob@example.com
END:VEVENT
END:VCALENDAR
```

Store the event URL or UID for deletion.

### Delete Event

```http
DELETE {calendarUrl}/{uuid}.ics
Authorization: Basic ...
```

### Apple Integration Limitations

| Limitation | Mitigation |
|------------|------------|
| No OAuth | App-specific password; educate users on rotation |
| No push webhooks | Poll every 5–15 minutes |
| Attendee invites less reliable | Send .ics email to guest separately |
| Rate limiting | Back off on 429/503; cache aggressively |

---

## Unified Sync Architecture

### Connection Record

Each host calendar connection stores:

```json
{
  "provider": "google | microsoft | apple",
  "accessToken": "encrypted",
  "refreshToken": "encrypted",
  "tokenExpiresAt": "2026-06-25T12:00:00Z",
  "providerAccountEmail": "alice@gmail.com",
  "calendars": [
    {
      "externalId": "primary",
      "name": "Alice Smith",
      "checkConflicts": true,
      "writeTarget": true
    }
  ],
  "status": "connected | expired | error",
  "lastSyncedAt": "2026-06-25T08:00:00Z"
}
```

### Sync Jobs

| Job | Frequency | Action |
|-----|-----------|--------|
| `refresh_tokens` | Every 30 min | Refresh tokens expiring within 10 min |
| `sync_free_busy` | Every 5–15 min | Fetch busy blocks for next 90 days |
| `push_booking_event` | On booking | Create event on write-target calendar |
| `update_booking_event` | On reschedule | PATCH/update external event |
| `delete_booking_event` | On cancel | Delete external event |

### Busy Cache Schema (Redis)

```
Key: busy:{userId}:{date}
Value: JSON array of { start: ISO8601, end: ISO8601, source: "google|microsoft|apple|booking" }
TTL: 300 seconds
```

Invalidate on: new booking, cancel, calendar settings change, webhook notification.

### Error Handling

| Error | Action |
|-------|--------|
| 401 Unauthorized | Mark connection `expired`; email host to reconnect |
| 403 Forbidden | Log; prompt host to re-authorize scopes |
| 429 Rate limited | Exponential backoff; retry up to 5 times |
| 5xx Provider error | Retry with jitter; alert if >3 consecutive failures |

---

## Provider Setup Checklists

### Google — Production Checklist

- [ ] OAuth consent screen verified (if using sensitive scopes broadly)
- [ ] Redirect URIs match production domain exactly
- [ ] Refresh tokens stored encrypted
- [ ] Calendar API quota monitored (default 1,000,000 queries/day)

### Microsoft — Production Checklist

- [ ] App registered for multitenant or single tenant as needed
- [ ] Admin consent granted for org accounts (if required)
- [ ] Client secret rotation schedule (every 12 months)
- [ ] Graph API throttling handled (429 + `Retry-After`)

### Apple — Production Checklist

- [ ] UI explains app-specific password requirement with link to Apple ID settings
- [ ] Credentials never logged
- [ ] Polling interval respects iCloud rate limits
- [ ] Fallback .ics email always sent to guests

---

## Testing Integrations

### Sandbox / Test Accounts

| Provider | Test approach |
|----------|---------------|
| Google | Use test Google account; Calendar API works in dev without verification for test users |
| Microsoft | Use Microsoft 365 developer sandbox or personal Outlook account |
| Apple | Use personal iCloud test account with app-specific password |

### Integration Test Scenarios

1. Connect calendar → calendars listed correctly.
2. External event created manually → slot blocked on public page.
3. Booking created → event appears on host calendar within 30 seconds.
4. Booking cancelled → event removed from host calendar.
5. Token expired → refresh succeeds automatically.
6. Token revoked → host notified; bookings still work but no external sync.

---

## Security Notes

1. **Never** store OAuth client secrets or refresh tokens in frontend code.
2. Encrypt all tokens at rest (AES-256-GCM with per-row IV).
3. Use `state` parameter on OAuth flows to prevent CSRF.
4. Revoke tokens at provider on disconnect:
   - Google: `POST https://oauth2.googleapis.com/revoke?token={token}`
   - Microsoft: no revoke endpoint; delete refresh token locally and remove app access in Azure
5. Apple app-specific passwords: advise users to revoke if compromised.

---

## Related Documents

- [Complete Documentation](./COMPLETE_DOCUMENTATION.md)
- [API Reference](./API_REFERENCE.md)
- [Database Schema](./DATABASE_SCHEMA.md)
