# Calendar & Contacts API

> Production-grade REST API for calendar management, event scheduling, contacts, availability, and public booking. Built for humans, AI agents, and programmatic automation.

## Base URL

- Local: http://localhost:3019
- OpenAPI spec: GET /openapi.json
- Swagger UI: GET /docs

## Authentication

Two methods, both sent as HTTP headers:

- JWT: `Authorization: Bearer <access_token>` - short-lived (15 min), obtained via login/register
- API Key: `X-API-Key: <token>` - long-lived, scoped, created via POST /api-keys (recommended for agents)

If both headers are present, JWT takes precedence.

## Auth Endpoints

POST /auth/register - Create account. Body: {"email", "password" (>=10 chars), "timezone"?}. Returns {user, access_token, refresh_token}. No auth required.
POST /auth/login - Login. Body: {"email", "password"}. Returns {user, access_token, refresh_token}. No auth required.
POST /auth/refresh - Refresh tokens. Body: {"refresh_token"}. Returns {access_token, refresh_token}. No auth required.
POST /auth/logout - Revoke refresh token. Body: {"refresh_token"}. Returns {"ok": true}. Requires auth.
GET /auth/me - Get current user. Returns {"user": {...}}. Requires auth.

## User Endpoints

GET /users/me - Get profile. Returns {"user": {id, email, timezone, created_at, updated_at}}.
PUT /users/me - Update profile. Body: {"timezone"?}. Returns {"user": {...}}.
DELETE /users/me - Soft-delete account and all associated data. Returns {"ok": true}.

## API Key Endpoints

POST /api-keys - Create API key. Body: {"name", "scopes": {"calendars": ["read","write"], "events": ["read","write"], "contacts": ["read","write"], "availability": ["read"], "booking": ["write"]}}. Returns {id, name, created_at, token}. Token shown once.
GET /api-keys - List keys. Returns {"items": [...], "page": {limit, next_cursor}}.
DELETE /api-keys/{id} - Revoke key. Returns {"ok": true}.

## Calendar Endpoints (scope: calendars:read/write)

GET /calendars - List all calendars (owned + shared). Returns {"items": [Calendar], "page": {...}}.
POST /calendars - Create. Body: {"name" (1-80), "color"? (#RRGGBB)}. Returns {"calendar": Calendar}.
GET /calendars/{id} - Get by ID. Returns {"calendar": Calendar}.
PUT /calendars/{id} - Update. Body: {"name"?, "color"?, "is_public"? (owner only)}. When is_public is set to true, a public iCal feed URL (ical_url) is generated. Returns {"calendar": Calendar}.
DELETE /calendars/{id} - Soft-delete (owner only). Returns {"ok": true}.

### Calendar Sharing

POST /calendars/{id}/share - Share. Body: {"target": {"email": "..."}, "role": "editor"|"viewer"}. Owner only.
GET /calendars/{id}/members - List members. Returns {"items": [CalendarMember], "page": {...}}.
DELETE /calendars/{id}/members/{userID} - Remove member. Owner only. Cannot remove owner.

## Event Endpoints (scope: events:read/write)

GET /events?start=RFC3339&end=RFC3339 - List events in range. Optional: calendar_id, search, tag, limit (1-200), cursor. Recurring events expanded into occurrences. Returns {"items": [Event], "page": {...}}.
POST /events - Create. Body: {"calendar_id", "title" (1-140), "start_time", "end_time", "timezone" (IANA), "all_day"?, "description"?, "location"?, "recurrence_rule"? (RFC5545 RRULE), "reminders"? ([minutes]), "tags"? ([string])}. Returns {"event": Event}.
GET /events/{id} - Get with reminders, attendees, tags, attachments. Returns {"event": Event}.
PUT /events/{id} - Update. Body: any event fields. Returns {"event": Event}.
DELETE /events/{id} - Soft-delete. Returns {"ok": true}.

### Event Reminders

POST /events/{id}/reminders - Add reminders. Body: {"minutes_before": [5, 15, 60]} (0-10080). Returns {"event": Event}.
DELETE /events/{id}/reminders/{reminderID} - Remove. Returns {"ok": true}.

### Event Attendees

POST /events/{id}/attendees - Add. Body: {"attendees": [{"email": "..."} or {"user_id": "..."}]}. Returns {"event": Event}.
PUT /events/{id}/attendees/{attendeeID} - Update RSVP. Body: {"status": "accepted"|"declined"|"tentative"}. Returns {"event": Event}.
DELETE /events/{id}/attendees/{attendeeID} - Remove. Returns {"ok": true}.

## Contact Endpoints (scope: contacts:read/write)

GET /contacts - List. Optional: search, limit (1-200), cursor. Returns {"items": [Contact], "page": {...}}.
POST /contacts - Create. Body: {"first_name"?, "last_name"?, "email"?, "phone"?, "company"?, "notes"?}. At least one of first_name/last_name/email/phone required. Returns {"contact": Contact}.
GET /contacts/{id} - Get. Returns {"contact": Contact}.
PUT /contacts/{id} - Update. Returns {"contact": Contact}.
DELETE /contacts/{id} - Soft-delete. Returns {"ok": true}.

## Availability Endpoint (scope: availability:read)

GET /availability?calendar_id=UUID&start=RFC3339&end=RFC3339 - Returns busy blocks including expanded recurring events. Response: {"calendar_id", "range_start", "range_end", "busy": [{"start", "end", "event_id"}]}.

## Booking Endpoints

POST /calendars/{id}/booking-link - Create booking link (scope: booking:write, owner only). Body: {"duration_minutes", "buffer_minutes"?, "timezone", "working_hours": {"mon": [{"start": "HH:MM", "end": "HH:MM"}], ...}, "active"?}. Returns {"token", "public_url", "settings": {...}}.
GET /booking/{token}/availability?start=RFC3339&end=RFC3339 - Public, no auth. Returns {"token", "timezone", "duration_minutes", "slots": [{"start", "end"}]}.
POST /booking/{token}/reserve - Public, no auth. Body: {"name", "email", "slot_start", "slot_end", "notes"?}. Returns {"ok": true, "event": Event}. 409 if slot taken.

## ICS Import/Export

GET /calendars/{id}/export.ics - Export as ICS (scope: calendars:read). Full RFC 5545 support: VEVENT with VALARM (reminders), ATTENDEE, all-day events (VALUE=DATE), RRULE, DTSTART/DTEND, SUMMARY, DESCRIPTION, LOCATION, CREATED, LAST-MODIFIED. Returns text/calendar.
POST /calendars/import - Import ICS file (scope: calendars:write). Multipart form: calendar_id (uuid) + file (.ics). Parses VEVENT with VALARM, ATTENDEE, TZID, VALUE=DATE, RRULE. Returns {"ok": true, "imported": {"events": N}}.
POST /calendars/import-url - Import from iCal URL (scope: calendars:write). Body: {"calendar_id": uuid, "url": "https://..."}. Fetches the iCal feed and imports events. Supports http, https, webcal protocols. Returns {"ok": true, "imported": {"events": N}, "source": url}.
GET /cal/{token}/feed.ics - iCal feed. No auth required. Works for both public and private calendars. Public calendars use a shorter base64url token; private calendars use a 64-character SHA256 hex token. The ical_url is in the Calendar object. Subscribe in Google Calendar, Apple Calendar, Outlook, etc.

## Data Schemas

User: {id, email, timezone, created_at, updated_at}
Calendar: {id, name, color, is_public, ical_url?, role, created_at, updated_at}
Event: {id, calendar_id, title, description?, location?, start_time, end_time, timezone, all_day, recurrence_rule?, is_occurrence, occurrence_start_time?, occurrence_end_time?, created_by, updated_by, created_at, updated_at, reminders[], attendees[], tags[], attachments[]}
Contact: {id, first_name?, last_name?, email?, phone?, company?, notes?, created_at, updated_at}
Reminder: {id, minutes_before}
Attendee: {id, user_id?, email?, status}
CalendarMember: {user_id, email, role}
BusyBlock: {start, end, event_id}
TimeSlot: {start, end}

## Error Format

All errors: {"error": "message", "code": "CODE", "details"?: any}
Codes: VALIDATION_ERROR (400), AUTH_REQUIRED (401), AUTH_INVALID (401), FORBIDDEN (403), NOT_FOUND (404), CONFLICT (409), RATE_LIMITED (429), INTERNAL (500)

## Pagination

Cursor-based. Query params: limit (default 50, max 200), cursor (opaque string). Response: {"items": [...], "page": {"limit": N, "next_cursor": string or null}}. Null cursor means last page.

## Key Constraints

- Passwords: >= 10 characters
- Calendar names: 1-80 characters
- Event titles: 1-140 characters
- Colors: #RRGGBB hex
- Timezones: IANA names (e.g. America/New_York, UTC)
- Recurrence: RFC 5545 RRULE (e.g. FREQ=WEEKLY;BYDAY=MO,WE,FR)
- Reminders: 0-10080 minutes before
- Event list range: max 1 year
- Soft deletion throughout (data recoverable)
- Calendar roles: owner (full), editor (events CRUD), viewer (read-only)
- Only owners can share calendars, delete calendars, create booking links, change is_public
- All calendars have an ical_url: public use shorter token, private use 64-char SHA256 hex for security
