# 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 ` - short-lived (15 min), obtained via login/register - API Key: `X-API-Key: ` - 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