12 KiB
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 (defaulthttp://localhost:3019)EMAIL/PASSWORD- account credentials used for login and token refreshAPI_KEY- scoped API key for long-lived programmatic access (preferred for agents)ACCESS_TOKEN/REFRESH_TOKEN- short-lived JWT tokens from login
Credential Priority
- Read
calendarcredentials.txtfirst. - If an
API_KEYis present, use it viaX-API-Keyheader for all requests. - If only
ACCESS_TOKENis available, useAuthorization: Bearer <ACCESS_TOKEN>. - If the access token returns 401, call
POST /auth/refreshwith the refresh token, update the file with new tokens, and retry. - If no tokens exist, call
POST /auth/loginwith 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
- Check if
calendarcredentials.txtexists. - If not: register, create API key, save credentials.
- 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) |
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:
{"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:
{
"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
GET /calendars- pick the target calendar.GET /availability?calendar_id=<id>&start=...&end=...- find a free slot.POST /events- create the event in the free slot.POST /events/{id}/attendees- invite attendees by email.POST /events/{id}/reminders- set reminders.
Workflow: Find free time across calendars
GET /calendars- list all calendars.- For each calendar:
GET /availability?calendar_id=<id>&start=...&end=... - Compute intersection of free time.
Workflow: Set up a public booking page
GET /calendars- pick the calendar.POST /calendars/{id}/booking-link- create the link with working hours.- Share the returned
public_urlortokenwith the person who needs to book.
Workflow: Import external calendar
GET /calendarsorPOST /calendars- ensure target calendar exists.POST /calendars/import- upload.icsfile as multipart form data withcalendar_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.