- Public/private: toggle is_public via PUT /calendars/{id}; generate/clear
public_token and return ical_url when public
- Public feed: GET /cal/{token}/feed.ics (no auth) for subscription in
Google/Apple/Outlook calendars
- Full iCal export: use golang-ical; VALARM, ATTENDEE, all-day (VALUE=DATE),
RRULE, DTSTAMP, CREATED, LAST-MODIFIED
- Full iCal import: parse TZID, VALUE=DATE, VALARM, ATTENDEE, RRULE
- Import from URL: POST /calendars/import-url with calendar_id + url
- Migration: unique index on public_token, calendar_subscriptions table
- Config: BASE_URL for ical_url; Calendar model + API: ical_url field
- Docs: OpenAPI, llms.txt, README, SKILL.md, about/overview
Made-with: Cursor
133 lines
8.2 KiB
Plaintext
133 lines
8.2 KiB
Plaintext
# 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 - Public iCal feed. No auth required. Returns the calendar's events in iCal format. Subscribe to this URL in Google Calendar, Apple Calendar, Outlook, etc. The URL is available as ical_url in the Calendar object when is_public is true.
|
|
|
|
## 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
|
|
- Public calendars provide an ical_url for subscription by external calendar apps
|