# Calendar & Contacts API - Agent Skill ## Purpose This skill enables AI agents to manage calendars, events, contacts, availability, and bookings through the Calendar & Contacts REST API. The API runs on `http://localhost:3019` by default. ## Credentials Credentials are stored in a file called `calendarcredentials.txt` in the agent's working directory or project root. If the file does not exist, create it after registration. ### Format of calendarcredentials.txt ``` BASE_URL=http://localhost:3019 EMAIL=agent@example.com PASSWORD=securepassword123 API_KEY= ACCESS_TOKEN= REFRESH_TOKEN= ``` - `BASE_URL` - the API server URL (default `http://localhost:3019`) - `EMAIL` / `PASSWORD` - account credentials used for login and token refresh - `API_KEY` - scoped API key for long-lived programmatic access (preferred for agents) - `ACCESS_TOKEN` / `REFRESH_TOKEN` - short-lived JWT tokens from login ### Credential Priority 1. Read `calendarcredentials.txt` first. 2. If an `API_KEY` is present, use it via `X-API-Key` header for all requests. 3. If only `ACCESS_TOKEN` is available, use `Authorization: Bearer `. 4. If the access token returns 401, call `POST /auth/refresh` with the refresh token, update the file with new tokens, and retry. 5. If no tokens exist, call `POST /auth/login` with email/password, store the returned tokens, and proceed. ## Authentication ### Two auth methods | Method | Header | Lifetime | Best for | |--------|--------|----------|----------| | JWT | `Authorization: Bearer ` | 15 min access / 7-30 day refresh | Interactive sessions | | API Key | `X-API-Key: ` | Until revoked | Agents and automation | ### Register a new account ``` POST /auth/register Content-Type: application/json {"email": "agent@example.com", "password": "securepassword123", "timezone": "UTC"} ``` Returns `user`, `access_token`, `refresh_token`. Save all to `calendarcredentials.txt`. ### Login ``` POST /auth/login Content-Type: application/json {"email": "agent@example.com", "password": "securepassword123"} ``` Returns `user`, `access_token`, `refresh_token`. ### Refresh tokens ``` POST /auth/refresh Content-Type: application/json {"refresh_token": ""} ``` Returns new `access_token` and `refresh_token`. Update `calendarcredentials.txt`. ### Create an API key (recommended for agents) ``` POST /api-keys Authorization: Bearer Content-Type: application/json { "name": "agent-full-access", "scopes": { "calendars": ["read", "write"], "events": ["read", "write"], "contacts": ["read", "write"], "availability": ["read"], "booking": ["write"] } } ``` Response includes a `token` field. This is the API key. It is only returned once. Save it to `calendarcredentials.txt` immediately. ### Agent bootstrap sequence 1. Check if `calendarcredentials.txt` exists. 2. If not: register, create API key, save credentials. 3. If yes: read credentials and authenticate using API key or JWT. ## API Reference Base URL: `http://localhost:3019` All request/response bodies are JSON. All timestamps are RFC3339 UTC. All list endpoints return `{"items": [...], "page": {"limit": N, "next_cursor": "..."}}`. ### Calendars | Method | Endpoint | Scope | Description | |--------|----------|-------|-------------| | GET | `/calendars` | calendars:read | List all calendars (owned + shared) | | POST | `/calendars` | calendars:write | Create a calendar | | GET | `/calendars/{id}` | calendars:read | Get a calendar by ID | | PUT | `/calendars/{id}` | calendars:write | Update name, color, is_public | | DELETE | `/calendars/{id}` | calendars:write | Soft-delete (owner only) | | POST | `/calendars/{id}/share` | calendars:write | Share with another user by email | | GET | `/calendars/{id}/members` | calendars:read | List calendar members and roles | | DELETE | `/calendars/{id}/members/{userID}` | calendars:write | Remove a member (owner only) | #### Create calendar ``` POST /calendars {"name": "Work", "color": "#22C55E"} ``` #### Share a calendar ``` POST /calendars/{id}/share {"target": {"email": "other@example.com"}, "role": "editor"} ``` ### Events | Method | Endpoint | Scope | Description | |--------|----------|-------|-------------| | GET | `/events?start=...&end=...` | events:read | List events in time range | | POST | `/events` | events:write | Create an event | | GET | `/events/{id}` | events:read | Get event with reminders/attendees | | PUT | `/events/{id}` | events:write | Update an event | | DELETE | `/events/{id}` | events:write | Soft-delete an event | #### List events (required: start, end) ``` GET /events?start=2026-03-01T00:00:00Z&end=2026-03-31T23:59:59Z&calendar_id= ``` Optional filters: `calendar_id`, `search`, `tag`, `limit` (max 200), `cursor`. Recurring events are automatically expanded into individual occurrences within the requested range. Occurrences have `is_occurrence: true` with `occurrence_start_time` / `occurrence_end_time`. #### Create event ``` POST /events { "calendar_id": "", "title": "Team standup", "start_time": "2026-03-01T14:00:00Z", "end_time": "2026-03-01T14:30:00Z", "timezone": "America/New_York", "all_day": false, "recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,WE,FR", "reminders": [10, 60], "tags": ["work", "standup"] } ``` #### Update event ``` PUT /events/{id} {"title": "Updated title", "start_time": "2026-03-01T15:00:00Z", "end_time": "2026-03-01T16:00:00Z"} ``` ### Reminders | Method | Endpoint | Scope | Description | |--------|----------|-------|-------------| | POST | `/events/{id}/reminders` | events:write | Add reminders (minutes before) | | DELETE | `/events/{id}/reminders/{reminderID}` | events:write | Remove a reminder | ``` POST /events/{id}/reminders {"minutes_before": [5, 15, 60]} ``` ### Attendees | Method | Endpoint | Scope | Description | |--------|----------|-------|-------------| | POST | `/events/{id}/attendees` | events:write | Add attendees by email or user_id | | PUT | `/events/{id}/attendees/{attendeeID}` | events:write | Update RSVP status | | DELETE | `/events/{id}/attendees/{attendeeID}` | events:write | Remove attendee | ``` POST /events/{id}/attendees {"attendees": [{"email": "guest@example.com"}, {"user_id": ""}]} ``` Status values: `pending`, `accepted`, `declined`, `tentative`. ### Contacts | Method | Endpoint | Scope | Description | |--------|----------|-------|-------------| | GET | `/contacts` | contacts:read | List contacts (optional: search, limit, cursor) | | POST | `/contacts` | contacts:write | Create a contact | | GET | `/contacts/{id}` | contacts:read | Get a contact | | PUT | `/contacts/{id}` | contacts:write | Update a contact | | DELETE | `/contacts/{id}` | contacts:write | Soft-delete a contact | ``` POST /contacts {"first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "phone": "+15551234567", "company": "Acme", "notes": "Met at conference"} ``` At least one identifying field (first_name, last_name, email, or phone) is required. ### Availability | Method | Endpoint | Scope | Description | |--------|----------|-------|-------------| | GET | `/availability?calendar_id=...&start=...&end=...` | availability:read | Get busy blocks for a calendar | ``` GET /availability?calendar_id=&start=2026-03-01T00:00:00Z&end=2026-03-07T23:59:59Z ``` Returns `busy` array of `{start, end, event_id}` blocks. Includes expanded recurring event occurrences. ### Booking (public, no auth required for GET availability and POST reserve) | Method | Endpoint | Auth | Description | |--------|----------|------|-------------| | POST | `/calendars/{id}/booking-link` | booking:write | Create a public booking link | | GET | `/booking/{token}/availability?start=...&end=...` | None | Get available slots | | POST | `/booking/{token}/reserve` | None | Reserve a time slot | #### Create booking link ``` POST /calendars/{id}/booking-link { "duration_minutes": 30, "buffer_minutes": 0, "timezone": "America/New_York", "working_hours": { "mon": [{"start": "09:00", "end": "17:00"}], "tue": [{"start": "09:00", "end": "17:00"}], "wed": [{"start": "09:00", "end": "17:00"}], "thu": [{"start": "09:00", "end": "17:00"}], "fri": [{"start": "09:00", "end": "17:00"}], "sat": [], "sun": [] }, "active": true } ``` #### Reserve a slot ``` POST /booking/{token}/reserve {"name": "Visitor", "email": "visitor@example.com", "slot_start": "2026-03-03T10:00:00Z", "slot_end": "2026-03-03T10:30:00Z", "notes": "Intro call"} ``` Returns 409 CONFLICT if the slot is no longer available. ### ICS Import/Export | Method | Endpoint | Scope | Description | |--------|----------|-------|-------------| | GET | `/calendars/{id}/export.ics` | calendars:read | Export calendar as ICS file | | POST | `/calendars/import` | calendars:write | Import ICS file (multipart/form-data) | Export returns `Content-Type: text/calendar`. Import requires multipart form with `calendar_id` (uuid) and `file` (.ics file). ### Users | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/users/me` | Get current user profile | | PUT | `/users/me` | Update timezone | | DELETE | `/users/me` | Soft-delete account and all data | ### API Keys | Method | Endpoint | Description | |--------|----------|-------------| | POST | `/api-keys` | Create API key with scopes | | GET | `/api-keys` | List API keys | | DELETE | `/api-keys/{id}` | Revoke an API key | ### Auth | Method | Endpoint | Auth | Description | |--------|----------|------|-------------| | POST | `/auth/register` | None | Create account | | POST | `/auth/login` | None | Login | | POST | `/auth/refresh` | None | Refresh JWT tokens | | POST | `/auth/logout` | JWT | Revoke refresh token | | GET | `/auth/me` | JWT/Key | Get authenticated user | ## Error Handling All errors return: ```json {"error": "Human-readable message", "code": "MACHINE_CODE", "details": "optional"} ``` | HTTP | Code | Meaning | |------|------|---------| | 400 | VALIDATION_ERROR | Invalid input | | 401 | AUTH_REQUIRED | No credentials provided | | 401 | AUTH_INVALID | Invalid or expired token | | 403 | FORBIDDEN | Insufficient permission or scope | | 404 | NOT_FOUND | Resource not found | | 409 | CONFLICT | Duplicate or slot unavailable | | 429 | RATE_LIMITED | Too many requests | | 500 | INTERNAL | Server error | On 401: refresh the access token or re-login. On 403 with an API key: the key may lack required scopes. On 429: back off and retry after a delay. ## Pagination All list endpoints use cursor-based pagination: ``` GET /events?start=...&end=...&limit=50&cursor= ``` Response always includes: ```json { "items": [], "page": {"limit": 50, "next_cursor": "abc123"} } ``` When `next_cursor` is `null`, there are no more pages. To fetch the next page, pass the cursor value as the `cursor` query parameter. ## Common Agent Workflows ### Workflow: Schedule a meeting 1. `GET /calendars` - pick the target calendar. 2. `GET /availability?calendar_id=&start=...&end=...` - find a free slot. 3. `POST /events` - create the event in the free slot. 4. `POST /events/{id}/attendees` - invite attendees by email. 5. `POST /events/{id}/reminders` - set reminders. ### Workflow: Find free time across calendars 1. `GET /calendars` - list all calendars. 2. For each calendar: `GET /availability?calendar_id=&start=...&end=...` 3. Compute intersection of free time. ### Workflow: Set up a public booking page 1. `GET /calendars` - pick the calendar. 2. `POST /calendars/{id}/booking-link` - create the link with working hours. 3. Share the returned `public_url` or `token` with the person who needs to book. ### Workflow: Import external calendar 1. `GET /calendars` or `POST /calendars` - ensure target calendar exists. 2. `POST /calendars/import` - upload `.ics` file as multipart form data with `calendar_id`. ## Important Constraints - Passwords must be at least 10 characters. - Calendar names: 1-80 characters. - Event titles: 1-140 characters. - Colors: hex format `#RRGGBB`. - Timezones: valid IANA timezone names (e.g., `America/New_York`, `UTC`). - Recurrence rules: RFC 5545 RRULE format (e.g., `FREQ=WEEKLY;BYDAY=MO,WE,FR`). - Reminder minutes_before: 0-10080 (up to 7 days). - Event time ranges for listing: max 1 year span. - Pagination limit: 1-200, default 50. - Contacts require at least one of: first_name, last_name, email, phone. - Only calendar owners can share, delete calendars, or create booking links. - Editors can create/update/delete events. Viewers are read-only. ## OpenAPI Spec The full OpenAPI 3.1.0 specification is available at `GET /openapi.json`. Interactive Swagger UI is at `GET /docs`.