Calendar
Knobs Calendar supports multiple calendars per workspace, single and recurring events, attendees with RSVPs, free/busy lookups, external ICS subscriptions, and a token-gated published ICS feed.
All routes are workspace-scoped and accept a session JWT or API key, except the public feed at the end. Calendars use the per-object ACL model (owner + grants + workspaceAccess) with one extra rung: freeBusy, which reveals busy intervals only. Event attendees can always read and RSVP to their own events regardless of calendar access.
Calendars
Section titled “Calendars”| Endpoint | Description |
|---|---|
GET /v1/workspaces/{wid}/calendar/calendars |
List calendars you can see (ensures your default personal calendar exists) |
POST /v1/workspaces/{wid}/calendar/calendars |
Create {name, color?, timeZone?} |
GET /v1/workspaces/{wid}/calendar/calendars/{cid} |
Calendar metadata |
PATCH /v1/workspaces/{wid}/calendar/calendars/{cid} |
Rename / recolor / retimezone |
DELETE /v1/workspaces/{wid}/calendar/calendars/{cid} |
Delete (the default calendar can’t be deleted) |
GET /v1/workspaces/{wid}/calendar/calendars/{cid}/access |
Sharing panel (incl. the freeBusy level) |
PUT /v1/workspaces/{wid}/calendar/calendars/{cid}/access |
Replace {workspaceAccess, grants} |
Events & instances
Section titled “Events & instances”Render views from instances — the server expands recurrences and applies ACL/privacy filtering:
| Endpoint | Description |
|---|---|
GET /v1/workspaces/{wid}/calendar/instances |
Expanded instances; ?from=&to= (RFC 3339, required) and ?calendarIds= (comma-separated) |
POST /v1/workspaces/{wid}/calendar/events |
Create an event (single or recurring) |
GET /v1/workspaces/{wid}/calendar/events/{eid} |
The event document (series master, single, or override) |
PATCH /v1/workspaces/{wid}/calendar/events/{eid} |
Edit; on a series add ?mode=single|following|all&originalStart= |
DELETE /v1/workspaces/{wid}/calendar/events/{eid} |
Delete/cancel with the same ?mode=&originalStart= scoping |
POST /v1/workspaces/{wid}/calendar/events/{eid}/rsvp |
{response, originalStart?} — attendee self-service (accepted/declined/tentative; per-occurrence when originalStart is set) |
Create an event
Section titled “Create an event”curl -X POST https://api.knobs.io/v1/workspaces/{wid}/calendar/events \ -H "Authorization: Bearer knak_..." \ -H "Content-Type: application/json" \ -d '{ "calendarId": "c_11ff…", "title": "Sprint planning", "startLocal": "2026-07-21T10:00:00", "endLocal": "2026-07-21T11:00:00", "timeZone": "America/New_York", "recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=TU"], "attendeeEmails": ["sam@acme.knobs.io"], "reminders": [{"method": "notification", "minutesBefore": 10}] }'Times are wall-clock local (startLocal/endLocal) plus an IANA timeZone — recurring events stay at the same local time across DST. allDay: true with date-only values creates all-day events. Recurrence is RFC 5545 RRULE strings.
Edit one occurrence vs. the series
Section titled “Edit one occurrence vs. the series”curl -X PATCH "https://api.knobs.io/v1/workspaces/{wid}/calendar/events/{eid}?mode=single&originalStart=2026-07-28T14:00:00Z" \ -H "Authorization: Bearer knak_..." \ -H "Content-Type: application/json" \ -d '{"title": "Sprint planning (moved)", "startLocal": "2026-07-28T11:00:00", "endLocal": "2026-07-28T12:00:00"}'mode=single overrides one occurrence, mode=following splits the series, mode=all edits the master.
Free/busy & settings
Section titled “Free/busy & settings”| Endpoint | Description |
|---|---|
POST /v1/workspaces/{wid}/calendar/freebusy |
{uids?[], emails?[], from, to} → merged busy blocks + working hours per target (intervals only, never titles) |
GET /v1/workspaces/{wid}/calendar/settings |
Your display timezone, working hours, default reminders |
PUT /v1/workspaces/{wid}/calendar/settings |
Replace those settings |
Quick-add, extraction & source links
Section titled “Quick-add, extraction & source links”| Endpoint | Description |
|---|---|
POST /v1/workspaces/{wid}/calendar/events:quick-add |
NLP quick-add {text, tz, confirm} — confirm:false returns {draft} for review; confirm:true creates → {draft, event}. Rate-limited |
POST /v1/workspaces/{wid}/calendar/events:extract-from-mail |
LLM event draft from a thread {threadId, tz?} → {draft, sourceRef} |
POST /v1/workspaces/{wid}/calendar/events:create-from-mail |
Non-AI source-link create {objectId, title?} from a mail thread |
POST /v1/workspaces/{wid}/calendar/events:create-from-chat |
Same from a chat message |
POST /v1/workspaces/{wid}/calendar/invites:import |
Import an inbound iCalendar invite {messageId, respond?} — adds it to your default calendar and optionally sends the iTIP reply |
Subscriptions (external ICS)
Section titled “Subscriptions (external ICS)”| Endpoint | Description |
|---|---|
GET /v1/workspaces/{wid}/calendar/subscriptions |
List ICS subscriptions |
POST /v1/workspaces/{wid}/calendar/subscriptions |
Add {url, name} — creates a read-only overlay calendar |
DELETE /v1/workspaces/{wid}/calendar/subscriptions/{sid} |
Remove (and its overlay calendar) |
POST /v1/workspaces/{wid}/calendar/subscriptions/{sid}/poll |
Poll the feed now |
Subscribed calendars are read-only (403 read_only_calendar on event writes).
Publishing a calendar (ICS feed)
Section titled “Publishing a calendar (ICS feed)”Owners can publish a calendar behind a capability token:
| Endpoint | Description |
|---|---|
POST /v1/workspaces/{wid}/calendar/calendars/{cid}/feed/rotate |
Mint/rotate the publish token → {token, path} — shown once; rotating invalidates the old URL |
The published feed itself is the platform’s one anonymous data-bearing GET (note: not under /v1):
GET /calendar-feeds/{cid}/{token}/calendar.icscurl https://api.knobs.io/calendar-feeds/{cid}/{token}/calendar.icsIt requires no bearer, is rate-limited per IP, answers a uniform 404 to a bad calendar or token, and exports private events as busy placeholders. Paste the URL into any calendar app that supports ICS subscriptions.
Filtering instances by attendee
Section titled “Filtering instances by attendee”GET …/calendar/instances accepts repeatable attendeeEmail and attendeeDomain parameters
(≤30 each), unioned with each other.
These match external attendees, which the ordinary attendee lookup cannot: it filters on member uids, and someone outside the workspace has none.
The filter narrows what you could already see; it never widens it. Instances you have no access to do not appear because you named someone on them.
A busy-only instance — one downgraded for privacy or free/busy-tier access — carries no attendees at all, so it can never match an attendee filter and drops out of a filtered response entirely. That is deliberate: matching it would itself reveal that someone on your filter list is on a private event.
OAuth scopes
Section titled “OAuth scopes”For third-party OAuth tokens. Reads and writes are separate grants.
| Routes | Scope |
|---|---|
GET …/calendar/calendars, …/calendars/{cid}, …/calendars/{cid}/access, …/instances, …/events/{eid}, …/settings, …/subscriptions; POST …/calendar/freebusy |
calendar:read |
POST/PATCH/DELETE …/calendar/calendars…, PUT …/access, POST/PATCH/DELETE …/calendar/events…, POST …/events/{eid}/rsvp, PUT …/settings, subscription create/delete/poll, POST …/calendar/invites:import |
calendar:write |
POST …/calendar/events:quick-add |
calendar:ai |
POST …/calendar/events:extract-from-mail |
calendar:ai and mail:read |
POST …/calendar/events:create-from-mail |
calendar:write and mail:read |
POST …/calendar/events:create-from-chat, POST …/calendars/{cid}/feed/rotate |
none — OAuth tokens are refused (Chat has no OAuth scope; rotating a feed token mints a credential) |
freebusy is a read despite being a POST — the body carries the target list, not a change.