Files
CalendarApi/SKILL.md
Michilis 2cb9d72a7f Add public/private calendars, full iCal support, and iCal URL import
- 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
2026-02-28 04:48:53 +00:00

14 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 (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.

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
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:

{"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

  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.