- 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
411 lines
14 KiB
Markdown
411 lines
14 KiB
Markdown
# 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=<api-key-token>
|
|
ACCESS_TOKEN=<current-jwt-access-token>
|
|
REFRESH_TOKEN=<current-jwt-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 <ACCESS_TOKEN>`.
|
|
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 <token>` | 15 min access / 7-30 day refresh | Interactive sessions |
|
|
| API Key | `X-API-Key: <token>` | 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": "<refresh_token>"}
|
|
```
|
|
|
|
Returns new `access_token` and `refresh_token`. Update `calendarcredentials.txt`.
|
|
|
|
### Create an API key (recommended for agents)
|
|
|
|
```
|
|
POST /api-keys
|
|
Authorization: Bearer <access_token>
|
|
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=<uuid>
|
|
```
|
|
|
|
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": "<uuid>",
|
|
"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": "<uuid>"}]}
|
|
```
|
|
|
|
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=<uuid>&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) |
|
|
| POST | `/calendars/import-url` | calendars:write | Import from external iCal URL |
|
|
| GET | `/cal/{token}/feed.ics` | None (public) | Public iCal feed for subscription |
|
|
|
|
Export returns `Content-Type: text/calendar` with full RFC 5545 support (VALARM, ATTENDEE, all-day events, RRULE).
|
|
|
|
Import requires multipart form with `calendar_id` (uuid) and `file` (.ics file). Supports VALARM (reminders), ATTENDEE, TZID, VALUE=DATE (all-day).
|
|
|
|
Import URL accepts JSON body: `{"calendar_id": "uuid", "url": "https://..."}`. Supports http, https, and webcal protocols.
|
|
|
|
Public feed: Set `is_public` to `true` via `PUT /calendars/{id}` to generate an `ical_url`. This URL can be used to subscribe in Google Calendar, Apple Calendar, Outlook, etc.
|
|
|
|
### 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=<opaque-string>
|
|
```
|
|
|
|
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=<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=<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`.
|
|
Or: `POST /calendars/import-url` - import from an iCal URL with `{"calendar_id": "...", "url": "https://..."}`.
|
|
|
|
### Workflow: Make a calendar publicly subscribable
|
|
|
|
1. `PUT /calendars/{id}` with `{"is_public": true}` - generates a public iCal feed URL.
|
|
2. The response includes `ical_url` (e.g., `https://api.example.com/cal/{token}/feed.ics`).
|
|
3. Share the `ical_url` - anyone can subscribe to it in their calendar app.
|
|
4. To revoke: `PUT /calendars/{id}` with `{"is_public": false}` - removes the feed URL.
|
|
|
|
## 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`.
|