CalendarSync API
A developer-friendly REST API to read your meetings, schedule events, check and trigger syncs, and pull AI-generated notes. Same data your dashboard sees, accessible to your scripts, automations, and AI agents.
Authentication
Send your key as a Bearer token. Generate one in settings.
Bearer cs_live_…
Base URL
JSON in, JSON out. Routes live under /api/v1/….
Rate limits
100 requests / minute per account. Watch X-RateLimit-Remaining in response headers. On a 429, wait the seconds in Retry-After, then retry.
Quick start
From API key to working response in 3 curl commands.
1. Sanity check
curl https://www.calendarsync.app/api/v1/status \
-H "Authorization: Bearer cs_live_your_key"2. List today's meetings
curl "https://www.calendarsync.app/api/v1/meetings?day=today" \
-H "Authorization: Bearer cs_live_your_key"3. Schedule something
curl -X POST https://www.calendarsync.app/api/v1/events \
-H "Authorization: Bearer cs_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Quick sync",
"start": "2026-04-26T07:00:00Z",
"end": "2026-04-26T07:30:00Z",
"attendees": [{"email": "friend@x.com"}]
}'Endpoints
Account
Quick health check on the account that owns the API key.
/api/v1/statusAccount-wide sync health. Connection count, active/erroring sync pairs, recent errors.
Response
{
"status": "healthy",
"connections": 3,
"sync_pairs": { "total": 3, "active": 3, "errors": 0 },
"recent_errors_24h": 0,
"timestamp": "2026-04-25T12:30:00.000Z"
}Calendars
List the connected calendar accounts (Google, Microsoft, Apple).
/api/v1/connectionsReturns every calendar connection with its provider, email, status, and the last time one of its sync pairs ran. Use the id when scheduling events.
Response
{
"data": [
{
"id": "abd156d6-...",
"provider": "google",
"email": "you@gmail.com",
"calendar_id": "primary",
"calendar_name": "you@gmail.com",
"status": "active",
"last_synced_at": "2026-04-25T12:30:00Z",
"created_at": "2026-04-11T10:00:00Z"
}
],
"count": 1
}Sync pairs
Inspect, trigger, and audit the calendar-to-calendar sync pairs you set up in the dashboard.
/api/v1/sync-pairsList every sync pair with mode, direction, status, and the underlying connections on each side.
Response
{
"data": [
{
"id": "de1168f5-...",
"sync_mode": "mirror",
"direction": "two_way",
"is_active": true,
"status": "idle",
"last_synced_at": "2026-04-25T12:30:00Z",
"connection_a": { "id": "...", "provider": "google", "email": "a@x.com" },
"connection_b": { "id": "...", "provider": "google", "email": "b@y.com" }
}
],
"count": 1
}/api/v1/sync-pairs/{id}/triggerRun a sync for one pair right now. Useful when an instant-sync webhook was missed or you want to verify a specific change propagated. One manual run per pair per minute (429 with Retry-After), and 409 if the pair is already syncing.
Response
{
"success": true,
"data": {
"events_created": 1,
"events_updated": 0,
"events_deleted": 0,
"duration_ms": 4200,
"trigger_type": "manual"
}
}/api/v1/sync-pairs/{id}/logsRecent sync events for one pair. Supports ?limit (default 20, max 100) and ?offset.
Query params
limitMax rows (1-100). Default 20.offsetSkip N rows. Default 0.
Response
{
"data": [
{
"id": "log-...",
"trigger_type": "webhook",
"level": "info",
"events_created": 1,
"duration_ms": 843,
"created_at": "2026-04-25T12:30:00Z"
}
],
"count": 1,
"limit": 20,
"offset": 0
}Meetings
List, read, update, and log meetings. Mirrors what you see on the dashboard's Meetings page.
/api/v1/meetingsList meetings with flexible filters. Each row includes attendees, event time, status, AI-generated summary, and structured action items when matched. Add compact=1 to leave the summary and action items out.
Query params
dayShortcut: today | tomorrow | yesterday (in your stored timezone). Overrides from/to.fromISO timestamp lower bound (inclusive).toISO timestamp upper bound (exclusive).statusComma-separated list of statuses to include. Default: today,active. Possible: today, active, done, notes.qSearch meeting titles (partial match) or an attendee's exact email address.limitMax rows (1-200). Default 50.compactSet to 1 to leave out meeting_summary and action_items. GET /api/v1/meetings/{id} still returns them.
Response
{
"meetings": [
{
"id": "mtg-...",
"title": "Kickoff with Sam",
"status": "today",
"event_date": "2026-04-26T09:00:00Z",
"is_all_day": false,
"attendees": [
{ "name": "Sam Lee", "email": "sam@example.com" }
],
"meeting_summary": null,
"action_items": [],
"matched_transcript_id": null,
"is_dismissed": false
}
],
"count": 1,
"timezone": "Asia/Singapore",
"window": { "from": "...", "to": "..." }
}/api/v1/meetings/{id}Full meeting record with notetaker-generated narrative and structured action items.
Response
{
"meeting": {
"id": "mtg-...",
"title": "Kickoff with Sam",
"event_date": "2026-04-26T09:00:00Z",
"meeting_summary": "Discussed scope, timelines, and onboarding...",
"action_items": [
{
"text": "Send the proposal by Friday",
"done": false,
"owner": "Ryan Tan",
"timestamp": "12:34"
}
]
}
}/api/v1/meetings/{id}Update one or more fields on a meeting. Use to mark a meeting done, dismiss, replace notes, or rewrite the action item list. Notes up to 20,000 characters; up to 100 action items of up to 1,000 characters each.
Request body
{
"status": "done",
"is_dismissed": false,
"meeting_summary": "Updated remarks here.",
"action_items": [
{ "text": "Email the deck", "done": false }
]
}Response
{
"meeting": {
"id": "mtg-...",
"status": "done",
"is_dismissed": false,
"updated_at": "2026-04-25T13:00:00Z"
}
}/api/v1/meetingsLog an ad-hoc or retrospective meeting (no calendar event behind it). Same as the dashboard's Log Meeting button. Title up to 500 characters, notes up to 20,000, up to 50 attendees.
Request body
{
"title": "Quick chat with Sam",
"event_date": "2026-04-25T08:30:00Z",
"is_all_day": false,
"attendees": [
{ "name": "Sam Lee", "email": "sam@example.com" }
],
"notes": "Aligned on next steps.",
"status": "active"
}Response
{
"meeting": {
"id": "mtg-...",
"title": "Quick chat with Sam",
"status": "active",
"event_date": "2026-04-25T08:30:00Z",
"meeting_summary": "Aligned on next steps.",
"created_at": "2026-04-25T13:00:00Z"
}
}Calendar events
Schedule new events on a connected calendar. The dashboard's calendar provider (Google / Microsoft / Apple) handles the write.
/api/v1/eventsCreate a calendar event. Pick which calendar via connection_id (omit it if you have only one active calendar of your own). Calendars other people connected through an invite can't be written to. Up to 10 events a minute and 200 a day per account; title up to 500 characters, description up to 8,000, up to 50 attendees.
Request body
{
"connection_id": "abd156d6-...",
"title": "Catch-up with Sam",
"start": "2026-04-26T07:00:00Z",
"end": "2026-04-26T07:30:00Z",
"description": "30-min check-in.",
"location": "Google Meet",
"attendees": [
{ "email": "sam@example.com", "displayName": "Sam Lee" }
],
"all_day": false
}Response
{
"event": {
"id": "google-evt-id",
"summary": "Catch-up with Sam",
"start": { "dateTime": "2026-04-26T07:00:00Z" },
"end": { "dateTime": "2026-04-26T07:30:00Z" },
"html_link": "https://calendar.google.com/...",
"attendees": [{ "email": "sam@example.com" }]
},
"connection": {
"id": "abd156d6-...",
"email": "you@gmail.com",
"provider": "google"
}
}Meeting notes
Run a Fireflies pull now. Useful right after a call to attach the transcript without waiting for the 15-min cron.
/api/v1/pull-notesPull recent notetaker transcripts and match them to your meetings that have no notes yet. Returns which tasks got matched. One pull every 2 minutes per account.
Request body
{
"window_days": 7
}Response
{
"matched": 1,
"total_tasks": 8,
"total_transcripts": 3,
"matches": [
{
"taskId": "mtg-...",
"taskTitle": "Kickoff with Sam",
"transcriptTitle": "Kickoff with Sam"
}
],
"skipped_reason": null
}Error codes
| Status | Meaning |
|---|---|
| 400 | Malformed request body, a missing required field, a field over its length limit, or a request the resource can't take (for example, triggering an inactive pair) |
| 401 | Missing or invalid API key |
| 403 | Key valid but the account is not on Pro or in its trial |
| 404 | Not found, including anything that belongs to another account |
| 409 | The sync pair is already syncing. Try again once it finishes. |
| 413 | Request body over 64 KB |
| 429 | Rate limited, or an endpoint's own limit reached. Wait the seconds in Retry-After, then retry. |
| 500 | Server error. Retry with backoff. |
| 503 | Temporarily unavailable. Wait the seconds in Retry-After, then retry. |
Invited connections
Accounts on a paid Pro plan can invite other people to connect their own calendar. Managed from the dashboard, not the API.
How it works
From Dashboard → Calendars, click “Invite someone”. We email them a single-use link that expires in 7 days, and you can copy it too. The invitee opens the link and connects Google or Outlook. Their calendar then appears in your connection list with an “Invited” badge. It can be the source of one-way sync pairs, and booking links can check it for conflicts, including a “Book Sam” link for that person. CalendarSync never syncs events into it, and a booking from their own link is written to it only if they turn that on from their manage link.
What the invitee sees
Who is asking (your name and email), a note that you will see their busy times only, and one Connect button per provider. No CalendarSync account is created for them. After connecting they receive an email with a private manage link where they can disconnect, or choose whether bookings made for them land on their calendar. Lost the link? The original invite page can resend it to the email on file.
Privacy
Invited calendars share busy times only: no event names, descriptions, locations or guests reach you, and nothing on your side can widen that. Either side can disconnect at any time; the other side is notified by email.
Billing
SGD 2 per invited connection per month by default. If your own Pro plan is monthly, it is added as a quantity line to that subscription. If your Pro plan is annual, it runs as a separate monthly subscription on the same card by default, so you are not charged a year up front for someone who may only be connected for a few weeks. If you prefer one renewal, the Seat billing control on the Billing page switches annual-plan seats to SGD 24 per person per year on the same invoice as your Pro plan; the remainder of your current plan year is charged straight away, prorated, and nothing is refunded mid-cycle. Annual seats are paid for the year: if someone disconnects, their seat stays available for the next person you invite at no extra charge until your renewal, when you are charged only for the seats in use. Switching back to monthly credits the unused part of the year to your plan renewal. Charged only when the invitee actually connects, prorated for the current period; a monthly seat is removed when they disconnect. Calendars you connect yourself are not counted.
Next: connect this to Claude
The CalendarSync MCP server turns every endpoint above except the sync logs into a tool your AI assistant can call directly. Ask it about your schedule, get notes, log meetings, and book events with plain English.
MCP setup guide →