# 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`.
