# Chronary — Full API Reference for LLMs > Calendar-as-a-service API for AI agents. This document covers the complete REST API surface, request/response schemas, error codes, rate limits, and webhook payloads. Base URL: `https://api.chronary.ai` OpenAPI spec: `https://api.chronary.ai/openapi.json` Docs: `https://docs.chronary.ai` --- ## Authentication All `/v1/` endpoints require a Bearer token in the Authorization header: ``` Authorization: Bearer chr_sk_ ``` Keys are created in the Chronary console at `https://console.chronary.ai`. Public endpoints that require no authentication: - `GET /health` — health check - `GET /ical/{ical_token}` — iCal feed (token acts as capability) - `GET /openapi.json` — this API spec --- ## Error Format All errors use a consistent JSON envelope: ```json { "error": { "type": "invalid_request", "message": "Human-readable description of the error.", "request_id": "req_01H9X4M2P5R8T6V0" } } ``` Common error types: - `invalid_request` — validation error (400) - `unauthorized` — missing or invalid API key (401) - `forbidden` — quota exceeded or operation not permitted (403) - `not_found` — resource not found (404) - `conflict` — duplicate resource (409) - `rate_limit_error` — 10 req/sec exceeded (429) — includes `Retry-After: 1` header - `internal_error` — unexpected server error (500) All error responses include `request_id` for tracing with support. --- ## Pagination List endpoints support cursor-based pagination via `limit` and `offset` query parameters: ``` GET /v1/agents?limit=50&offset=0 ``` Responses include: ```json { "data": [...], "total": 143 } ``` Default `limit` is 50 for most endpoints (20 for webhooks). Maximum is 200. --- ## Rate Limits & Quotas **Rate limit:** 10 requests per second per API key on Free; 50 req/s on Pro; custom on Custom. Exceeding returns HTTP 429 with `Retry-After: 1`. **Monthly quotas by plan:** | Metric | Free | Pro | Custom | |--------|------|-----|--------| | Agents | 3 | 50 | Custom | | Calendars | 10 | 250 | Custom | | Events | 2,500 | 125,000 | Custom | | API calls | 50,000 | 1,000,000 | Custom | | Webhook deliveries | 5,000 | 250,000 | Custom | | Availability queries | 10,000 | 1,000,000 | Custom | | iCal subscriptions | 5 | 100 | Custom | | Webhook endpoints | 3 | 25 | Custom | | Per-agent API keys | — | 50 | Custom | Quota exceeded → HTTP 429 with `type: "quota_exceeded"` and `Retry-After` set to seconds until the start of the next UTC month. **Plan capability gating (feature-level):** Pro plan unlocks four feature groups unavailable to free-tier orgs: | Feature | Endpoints | |---------|-----------| | Scheduling negotiation | All `POST/GET /v1/scheduling/proposals` routes | | Temporal holds | `POST /v1/calendars/:id/events` with `status:"hold"`, `PUT /v1/events/:id/confirm`, `PUT /v1/events/:id/release` | | Cross-agent availability | `GET /v1/availability` | | Agent-scoped API keys | All `POST/GET/DELETE /v1/keys` routes | Free-tier requests to Pro endpoints return HTTP 403 with: ```json { "error": { "type": "plan_required", "required_plan": "pro", "feature": "scheduling", "upgrade_url": "https://console.chronary.ai/settings/billing", "docs_url": "https://docs.chronary.ai/pricing" } } ``` Every authenticated response includes `X-Chronary-Plan: free|pro|scale`. --- ## System ### GET /health Health check. No authentication required. Response 200: ```json { "status": "ok", "ts": "2025-01-15T10:00:00.000Z" } ``` --- ## Agents Agents are the primary entity — they own calendars and have their own availability. Type can be `ai`, `human`, or `resource`. Agent ID prefix: `agt_` ### GET /v1/agents List all agents for the organization. Query parameters: - `type` (optional): Filter by `ai`, `human`, or `resource` - `status` (optional): Filter by `active`, `paused`, or `decommissioned` - `limit` (optional, default 50, max 200) - `offset` (optional, default 0) Response 200: ```json { "data": [ { "id": "agt_01H9X4M2P5R8T6V0", "name": "Scheduling Assistant", "type": "ai", "description": "Books meetings with prospects.", "status": "active", "metadata": {}, "created_at": "2025-01-15T10:00:00.000Z", "updated_at": "2025-01-15T10:00:00.000Z" } ], "total": 3 } ``` ### POST /v1/agents Register your agent with Chronary, creating a Chronary identity it can use to own calendars, events, and webhooks. The agent itself already exists in your system — this gives it a Chronary record. Counts against the agents quota. Request body: ```json { "name": "Scheduling Assistant", "type": "ai", "description": "Books meetings with prospects.", "metadata": {} } ``` Fields: - `name` (required): string, 1–255 chars - `type` (required): `"ai"` | `"human"` | `"resource"` - `description` (optional): string - `metadata` (optional): object, max 16KB Response 201: Agent object (same shape as GET response item) ### GET /v1/agents/{id} Get a single agent by ID. Response 200: Agent object ### PATCH /v1/agents/{id} Update an agent. At least one field required. Request body (all optional): ```json { "name": "Updated Name", "description": "Updated description.", "metadata": { "key": "value" }, "status": "paused" } ``` `status` options: `active`, `paused` (cannot set to `decommissioned` via PATCH — use DELETE) Response 200: Updated agent object ### DELETE /v1/agents/{id} Decommission the agent and cascade-delete all associated calendars and events. Response 204: No content --- ## Calendars Calendars belong to an agent (agent-owned) or to the organization (shared). Each calendar has an `ical_token` that can be used to serve the calendar as a public iCal feed. Calendars also expose an `agent_status` field so the owning agent can advertise operational state to other agents and human subscribers. Calendar ID prefix: `cal_` Calendar `agent_status` values (default `idle`): - `idle` — agent is free and not currently running - `working` — agent is actively processing work - `waiting` — agent is blocked waiting on an external dependency - `error` — agent has failed and needs attention The server does not enforce transitions — agents pick whichever value best describes their current state. Set it on create/update via the `agent_status` field, or read it back on any calendar response and on `GET /v1/calendars/:id/context`. ### GET /v1/agents/{agent_id}/calendars List calendars owned by a specific agent. Query parameters: - `limit` (optional, default 50, max 200) - `offset` (optional, default 0) Response 200: ```json { "data": [ { "id": "cal_01H9X4M2P5R8T6V0", "agent_id": "agt_01H9X4M2P5R8T6V0", "name": "Work Calendar", "timezone": "America/Los_Angeles", "ical_token": "abc123def456...", "agent_status": "idle", "default_reminders": null, "metadata": {}, "created_at": "2025-01-15T10:00:00.000Z", "updated_at": "2025-01-15T10:00:00.000Z" } ], "total": 2 } ``` ### POST /v1/agents/{agent_id}/calendars Create a calendar owned by a specific agent. Request body: ```json { "name": "Work Calendar", "timezone": "America/Los_Angeles", "agent_status": "idle", "metadata": {} } ``` Fields: - `name` (required): string, 1–255 chars - `timezone` (required): IANA timezone string (e.g. `"America/New_York"`, `"UTC"`) - `agent_status` (optional, default `"idle"`): `idle` | `working` | `waiting` | `error` - `default_reminders` (optional): integer[] | null — minutes before `start_time` that events inherit when they don't set their own `reminders` (e.g. `[10, 1440]`). Max 5 entries, each 1–40320 (28 days). `null` clears it. - `metadata` (optional): object, max 16KB Response 201: Calendar object ### GET /v1/calendars List all calendars for the organization. By default only returns agent-owned calendars. Query parameters: - `include` (optional): `"all"` to include shared org-level calendars - `limit` (optional, default 50, max 200) - `offset` (optional, default 0) Response 200: Same shape as agent-scoped list ### POST /v1/calendars Create a shared (org-level) calendar not associated with any agent. Request body: Same as agent-scoped create (without agent_id) Response 201: Calendar object (with `agent_id: null`) ### GET /v1/calendars/{id} Get a single calendar by ID. Response 200: Calendar object ### PATCH /v1/calendars/{id} Update a calendar. At least one field required. Request body (all optional): ```json { "name": "Updated Name", "timezone": "UTC", "agent_status": "working", "default_reminders": [10, 1440], "metadata": {} } ``` `agent_status` options: `idle` | `working` | `waiting` | `error`. `default_reminders` is an integer[] of minutes before start (max 5, each 1–40320) that events inherit when they don't set their own `reminders`; set to `null` to clear. Response 200: Updated calendar object ### DELETE /v1/calendars/{id} Delete a calendar and all its events. Response 204: No content ### GET /v1/calendars/{id}/context Returns a temporal snapshot for the calendar — the current event (if any), the next upcoming event, the last three events that ended, the next events starting within 24 hours, the server's current time, and the calendar's `agent_status`. Designed for agents that need a single-call answer to "what's happening right now on this calendar?" Response 200: ```json { "calendar_id": "cal_01H9X4M2P5R8T6V0", "now": "2026-04-16T14:30:00.000Z", "agent_status": "working", "current_event": { "id": "evt_01H9X4M2P5R8T6V0", "calendar_id": "cal_01H9X4M2P5R8T6V0", "title": "Team standup", "start_time": "2026-04-16T14:00:00.000Z", "end_time": "2026-04-16T15:00:00.000Z", "status": "confirmed" }, "next_event": { "id": "evt_01H9X4M2P5R8T6V1", "calendar_id": "cal_01H9X4M2P5R8T6V0", "title": "Design review", "start_time": "2026-04-16T16:00:00.000Z", "end_time": "2026-04-16T17:00:00.000Z", "status": "confirmed" }, "recent_events": [ { "id": "evt_01H9X4M2P5R8T6V2", "calendar_id": "cal_01H9X4M2P5R8T6V0", "title": "Weekly metrics digest", "start_time": "2026-04-16T13:00:00.000Z", "end_time": "2026-04-16T13:30:00.000Z", "status": "confirmed" } ], "upcoming": [ { "id": "evt_01H9X4M2P5R8T6V1", "calendar_id": "cal_01H9X4M2P5R8T6V0", "title": "Design review", "start_time": "2026-04-16T16:00:00.000Z", "end_time": "2026-04-16T17:00:00.000Z", "status": "confirmed" } ] } ``` `current_event` and `next_event` are `null` when nothing matches. `recent_events` contains up to the 3 most recently ended events, newest first. `upcoming` contains up to the next 5 events starting within 24 hours, earliest first. Cancelled and soft-deleted events are excluded from all four event lists. Does not consume the events quota. --- ## Events Events belong to a calendar and have a title, start/end time, status, and optional metadata. Event ID prefix: `evt_` Reminders: an event's `reminders` is an integer[] of minutes before `start_time` (e.g. `[10, 1440]` = 10 min and 1 day before; max 5 entries, each 1–40320 / 28 days). Resolution precedence is the event's own `reminders` → the calendar's `default_reminders` → the system default `[10]`. A `null` field inherits the level above; `[]` means explicitly no reminders. Each resolved reminder fires an `event.reminder` webhook at `start_time` minus the offset (confirmed events only) and is emitted as a `VALARM` in the iCal feed. ### GET /v1/agents/{agent_id}/events List events across all calendars owned by an agent. Query parameters: - `start_after` (optional): ISO 8601 datetime — filter events starting after this time - `start_before` (optional): ISO 8601 datetime — filter events starting before this time - `status` (optional): `confirmed` | `tentative` | `cancelled` - `source` (optional): `internal` | `external_ical` - `limit` (optional, default 50, max 200) - `offset` (optional, default 0) Response 200: ```json { "data": [ { "id": "evt_01H9X4M2P5R8T6V0", "calendar_id": "cal_01H9X4M2P5R8T6V0", "title": "Team standup", "description": "Daily sync meeting.", "start_time": "2025-01-20T09:00:00.000Z", "end_time": "2025-01-20T09:30:00.000Z", "all_day": false, "status": "confirmed", "source": "internal", "metadata": {}, "reminders": null, "created_at": "2025-01-15T10:00:00.000Z", "updated_at": "2025-01-15T10:00:00.000Z" } ], "total": 42 } ``` `reminders` is `null` when the event inherits its calendar's `default_reminders`. ### GET /v1/calendars/{cal_id}/events List events in a specific calendar. Same query parameters as agent-scoped list. Response 200: Same shape as agent-scoped list ### POST /v1/calendars/{cal_id}/events Create an event in a specific calendar. Counts against the events quota. Request body: ```json { "title": "Team standup", "start_time": "2025-01-20T09:00:00.000Z", "end_time": "2025-01-20T09:30:00.000Z", "description": "Daily sync meeting.", "all_day": false, "status": "confirmed", "metadata": {} } ``` Fields: - `title` (required): string, 1–500 chars - `start_time` (required): ISO 8601 datetime - `end_time` (required): ISO 8601 datetime — must be after `start_time` - `description` (optional): string - `all_day` (optional, default false): boolean - `status` (optional, default `"confirmed"`): `confirmed` | `tentative` | `cancelled` | `hold` - `hold_expires_at` (required when `status=hold`): ISO 8601 — must be 30 seconds to 15 minutes in the future - `hold_priority` (optional, with `status=hold`): integer 0–100; higher-priority holds pre-empt lower-priority overlapping holds - `reminders` (optional): integer[] | null — minutes before `start_time` to fire reminders (max 5, each 1–40320). `null` inherits the calendar's `default_reminders`; `[]` disables reminders. - `metadata` (optional): object, max 16KB Response 201: Event object ### PUT /v1/events/{id}/confirm Promote a tentative hold (`status=hold`) to a confirmed event. Only valid while the hold is still within its TTL. Returns `409` if the event is not a hold or if the hold has expired. Response 200: the updated event row with `status=confirmed`. Fires an `event.hold_confirmed` webhook. ### PUT /v1/events/{id}/release Manually release a held event before its TTL elapses, freeing the slot. Returns `409` if the event is not a hold. Response 200: the released event row. Fires an `event.hold_released` webhook. ### GET /v1/events/{id} Get a single event by ID. Response 200: Event object ### PATCH /v1/events/{id} Update an event. At least one field required. Request body (all optional): ```json { "title": "Updated title", "description": "Updated description.", "start_time": "2025-01-20T10:00:00.000Z", "end_time": "2025-01-20T10:30:00.000Z", "all_day": false, "status": "tentative", "reminders": [10, 1440], "metadata": {} } ``` `reminders` (integer[] | null) replaces the event's reminders; `null` reverts to inheriting the calendar's `default_reminders`, `[]` disables them. Response 200: Updated event object ### DELETE /v1/events/{id} Delete a single event. Response 204: No content --- ## Availability Returns free/busy time slots. All queries require `start`, `end`, and optionally `slot_duration`. Slot duration options: `15m`, `30m` (default), `45m`, `1h`, `2h` ### GET /v1/agents/{id}/availability Check availability across all calendars owned by a single agent. Query parameters: - `start` (required): ISO 8601 datetime - `end` (required): ISO 8601 datetime — must be after `start` - `slot_duration` (optional, default `30m`): `15m` | `30m` | `45m` | `1h` | `2h` - `include_busy` (optional, default `false`): `true` | `false` — include busy slots in response Response 200: ```json { "slots": [ { "start": "2025-01-20T09:00:00.000Z", "end": "2025-01-20T09:30:00.000Z", "available": true }, { "start": "2025-01-20T09:30:00.000Z", "end": "2025-01-20T10:00:00.000Z", "available": false } ] } ``` ### GET /v1/calendars/{id}/availability Check availability for a single calendar. Same query parameters as agent-scoped availability. Response 200: Same shape as agent availability ### GET /v1/availability Cross-agent availability — returns intersected free slots across multiple agents. Query parameters: - `agents` (required): comma-separated agent IDs (e.g. `agt_abc,agt_def,agt_ghi`) - `start` (required): ISO 8601 datetime - `end` (required): ISO 8601 datetime - `slot_duration` (optional, default `30m`) - `calendars` (optional): comma-separated calendar IDs to restrict the query - `include_busy` (optional, default `false`) Plan limits for cross-agent queries: - Free: max 5 agents, max 30-day range - Pro: max 20 agents, max 90-day range - Scale: max 50 agents, max 365-day range Response 200: Same shape as single-agent availability ### Availability rules Per-calendar rules that shape the busy blocks fed into the availability engine — use them to declare working hours and buffer times that don't require creating placeholder events. ### PUT /v1/calendars/{id}/availability-rules Replace the calendar's full rule set. Send the complete rules payload each time — rules are not merged. Request body: ```json { "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" } }, "buffer_before_minutes": 15, "buffer_after_minutes": 15, "timezone": "America/Los_Angeles" } ``` Fields: - `working_hours` (optional, nullable): object keyed by weekday abbreviation (`mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun`); each value is `{ start, end }` with `HH:MM` 24-hour strings. Omit a day to treat it as entirely non-working. At least one day must be present when the field is set. Pass `null` to clear. - `buffer_before_minutes` (optional): integer 0–120 - `buffer_after_minutes` (optional): integer 0–120 - `timezone` (optional): IANA timezone for interpreting working-hours times (defaults to `UTC`) Response 200: The stored rules object. ### GET /v1/calendars/{id}/availability-rules Return the calendar's current rules, or `null` fields if none set. Response 200: Rules object. ### DELETE /v1/calendars/{id}/availability-rules Clear all availability rules on the calendar. Response 204: No content. --- ## Scheduling Proposals > **Pro plan required.** Free-tier callers receive `403 plan_required`. Multi-agent scheduling negotiation. An organizer agent offers a set of candidate slots to one or more participant agents, collects their responses (accept, decline, or counter with alternative slots), and resolves the proposal into a confirmed event once there is agreement. Proposal ID prefix: `spr_` Proposal state machine: - `pending` — awaiting participant responses - `confirmed` — resolved; a winning slot was chosen and an event was created - `expired` — TTL reached before resolution - `cancelled` — cancelled by the organizer or auto-cancelled because every participant declined ### POST /v1/scheduling/proposals Create a proposal. Request body: ```json { "title": "Q2 planning sync", "description": "45 minutes to align on Q2 goals.", "organizer_agent_id": "agt_organizer", "participant_agent_ids": ["agt_alice", "agt_bob"], "calendar_id": "cal_team", "slots": [ { "start_time": "2025-04-20T15:00:00.000Z", "end_time": "2025-04-20T15:45:00.000Z", "weight": 2 }, { "start_time": "2025-04-21T15:00:00.000Z", "end_time": "2025-04-21T15:45:00.000Z" } ], "expires_at": "2025-04-19T00:00:00.000Z", "metadata": {} } ``` Fields: - `title` (required): 1–500 chars - `description` (optional): up to 5000 chars - `organizer_agent_id` (required): agent creating the proposal - `participant_agent_ids` (required): 1–50 agents - `calendar_id` (required): default target calendar for the resolved event - `slots` (required): 1–20 candidate `{ start_time, end_time, weight?, calendar_id? }` entries - `expires_at` (optional): ISO timestamp — proposal auto-expires if unresolved - `metadata` (optional): object, max 16KB Response 201: Proposal object. ### GET /v1/scheduling/proposals List proposals for the organization. Filter by `status`, `organizer_agent_id`, or `participant_agent_id`. Response 200: `{ "data": [proposal, ...], "total": n }`. ### GET /v1/scheduling/proposals/{id} Return a single proposal with all responses. ### POST /v1/scheduling/proposals/{id}/respond A participant responds to the proposal. At most one response per participant per proposal — a second attempt returns `409 conflict` with `type: "duplicate_response"`. Request body: ```json { "agent_id": "agt_alice", "response": "accept", "selected_slot_id": "slt_01H9X4A1B2C3D4E5F6" } ``` Fields: - `agent_id` (required): participant agent responding - `response` (required): `accept` | `decline` | `counter` - `selected_slot_id` (required when `response=accept`): the ID of the slot the agent is accepting, from the proposal's `slots[].id` array - `counter_slots` (optional, typically used with `response=counter`): array of candidate `{ start_time, end_time, weight?, calendar_id? }` entries the participant is proposing instead - `message` (optional): free-form note up to 2000 chars Response 200: the updated proposal object with the new response appended. ### POST /v1/scheduling/proposals/{id}/resolve Organizer resolves a pending proposal. Takes **no request body** — the server runs the resolver over the existing responses, picks the best slot using the scoring rules (accept=1.0, counter=0.3, decline=0.0, tiebreak by earliest `start_time`), and creates a confirmed event on the proposal's `calendar_id`. If every participant declined, the proposal transitions to `cancelled` with `reason: "all_declined"` instead. Response 200: ```json { "status": "confirmed", "resolved_slot": { "id": "slt_01H9X4...", "start_time": "...", "end_time": "...", "weight": 1.0, "calendar_id": "cal_team" } } ``` or ```json { "status": "cancelled", "reason": "all_declined" } ``` ### POST /v1/scheduling/proposals/{id}/cancel Cancel a pending proposal. Cannot be undone. Response 200: Proposal object with `status: "cancelled"`. --- ## Webhooks Chronary delivers signed POST requests to your URL when subscribed events occur. Webhook ID prefix: `whk_` Event types available for subscription (17 total): - `agent.created` - `agent.updated` - `event.created` — confirmed/tentative event created via `POST /v1/calendars/:cal_id/events` - `event.updated` - `event.deleted` - `event.started` — fired when a confirmed event's `start_time` is reached - `event.ended` — fired when a confirmed event's `end_time` is reached - `event.reminder` — fired ahead of a confirmed event's `start_time`, once per resolved reminder offset - `event.hold_created` — a tentative hold (`status=hold`) was created - `event.hold_expired` — a hold's TTL elapsed, or a higher-priority hold pre-empted it - `event.hold_released` — a hold was manually released via `PUT /v1/events/:id/release` - `event.hold_confirmed` — a hold was promoted via `PUT /v1/events/:id/confirm` - `proposal.created` - `proposal.responded` - `proposal.confirmed` - `proposal.expired` — pending proposal's `expires_at` was reached - `proposal.cancelled` — organizer cancel or all-declined auto-cancel ### GET /v1/webhooks List all registered webhooks. Query parameters: - `limit` (optional, default 20, max 100) - `offset` (optional, default 0) Response 200: ```json { "data": [ { "id": "whk_01H9X4M2P5R8T6V0", "url": "https://example.com/webhooks/chronary", "events": ["event.created", "event.updated"], "active": true, "created_at": "2025-01-15T10:00:00.000Z" } ], "total": 1 } ``` ### POST /v1/webhooks Register a new webhook endpoint. Request body: ```json { "url": "https://example.com/webhooks/chronary", "events": ["event.created", "event.updated", "event.deleted"] } ``` Fields: - `url` (required): HTTPS URL to receive webhook deliveries - `events` (required): array of event types — at least one required Response 201: ```json { "id": "whk_01H9X4M2P5R8T6V0", "url": "https://example.com/webhooks/chronary", "events": ["event.created"], "active": true, "secret": "whsec_abc123...", "created_at": "2025-01-15T10:00:00.000Z" } ``` **Important:** `secret` is returned only at creation time. Store it securely — it is used to verify HMAC-SHA256 signatures. ### GET /v1/webhooks/{id} Get a webhook by ID. The `secret` field is NOT returned after creation. Response 200: Webhook object (without secret) ### PATCH /v1/webhooks/{id} Update a webhook. Request body (all optional): ```json { "url": "https://new-url.example.com/webhooks", "events": ["agent.created"], "active": true } ``` Response 200: Updated webhook object ### DELETE /v1/webhooks/{id} Delete a webhook. Response 204: No content ### GET /v1/webhooks/{id}/deliveries Inspect delivery history and diagnose failures. Requires an org-level API key (`chr_sk_*`); agent-scoped keys receive 403. Query parameters: - `limit` (integer, default 20, max 100): results per page - `offset` (integer, default 0): pagination offset - `status` (string, optional): filter to `pending`, `delivered`, or `failed` - `include_payload` (boolean string, default `false`): pass `true` to include the full event payload in each delivery record. Always pass the value explicitly — do NOT send the bare key without a value. Response 200: ```json { "data": [ { "id": "whd_01H9X4M2P5R8T6V0", "subscription_id": "whk_01H9X4M2P5R8T6V0", "event_type": "event.created", "status": "failed", "attempts": 3, "last_attempt_at": "2026-04-18T10:00:00.000Z", "next_retry_at": null, "created_at": "2026-04-18T09:55:00.000Z" } ], "total": 42, "limit": 20, "offset": 0, "stats": { "pending": 2, "delivered": 35, "failed": 5 } } ``` `payload` is absent unless `include_payload=true`. `stats` reflects all-time counts for the webhook (not filtered by the `status` param) — use it to render a success-rate summary without extra round-trips. Errors: 400 invalid status value, 403 agent-scoped key, 404 webhook not found or not owned by caller. ### Webhook Payload Format Chronary delivers POST requests to your URL with: - `Content-Type: application/json` - `X-Signature: sha256=` — HMAC-SHA256 of `` `${X-Timestamp}.${body}` `` using your webhook secret - `X-Timestamp: ` — included in the signed string to mitigate replay - `X-Delivery-Id: whd_...` — idempotency key; retries reuse the same delivery ID Note: there is **no** `X-Event-Type` header. The event type is fixed by the subscription, so a consumer that needs to distinguish multiple event types should either (a) register one webhook URL per event type, or (b) infer the type from the payload shape. Option (a) is the recommended pattern. Signature verification (Node.js example): ```js const crypto = require('crypto'); function verifyChronarySignature(secret, timestamp, body, signatureHeader) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(`${timestamp}.${body}`) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signatureHeader), Buffer.from(expected), ); } ``` The body is the **raw event-specific payload** — there is no `{ type, data }` envelope. Shapes below match what the HTTP body will contain. `event.created` / `event.updated` / `event.hold_created` / `event.hold_confirmed`: ```json { "calendar_id": "cal_01H9X4M2P5R8T6V0", "event": { "id": "evt_01H9X4...", "title": "...", "...": "..." } } ``` `event.deleted` / `event.hold_expired` / `event.hold_released`: ```json { "calendar_id": "cal_01H9X4M2P5R8T6V0", "event_id": "evt_01H9X4M2P5R8T6V0" } ``` `agent.created` / `agent.updated` — camelCase agent record wrapped under `agent`: ```json { "agent": { "id": "agt_01H9X4M2P5R8T6V0", "orgId": "...", "name": "Booking Bot", "type": "ai", "description": null, "status": "active", "metadata": {}, "createdAt": "2026-04-16T12:00:00.000Z", "updatedAt": "2026-04-16T12:00:00.000Z" } } ``` `event.started` / `event.ended` — flat payload, no nested `event` object: ```json { "event_id": "evt_01H9X4M2P5R8T6V0", "calendar_id": "cal_01H9X4M2P5R8T6V0", "title": "Team standup", "start_time": "2026-04-16T09:00:00.000Z", "end_time": "2026-04-16T09:30:00.000Z" } ``` Fired automatically as confirmed events reach their `start_time` / `end_time`. Delivery is best-effort at ~seconds precision with a 30-second stale-fire tolerance — use this for "wake up when the meeting starts" patterns, not hard real-time triggers. `event.reminder` — flat payload, fires ahead of a confirmed event's `start_time` once per resolved reminder offset (`reminder_minutes` identifies which one): ```json { "event_id": "evt_01H9X4M2P5R8T6V0", "calendar_id": "cal_01H9X4M2P5R8T6V0", "title": "Team standup", "start_time": "2026-04-16T09:00:00.000Z", "end_time": "2026-04-16T09:30:00.000Z", "reminder_minutes": 10 } ``` Offsets resolve from the event's `reminders`, else the calendar's `default_reminders`, else the system default `[10]`. Confirmed events only; stale fires (rescheduled/cancelled/deleted) are suppressed. `proposal.created`: ```json { "proposal": { /* full proposal with slots[] and responses[] */ } } ``` `proposal.responded`: ```json { "proposal_id": "spr_01H9X4M2P5R8T6V0", "agent_id": "agt_01H9X4...", "response": "accept" } ``` `proposal.confirmed`: ```json { "proposal_id": "spr_01H9X4M2P5R8T6V0", "resolved_slot": { "id": "slt_01H9X4...", "start_time": "...", "end_time": "...", "weight": 1.0, "calendar_id": "cal_team" }, "created_event_id": "evt_01H9X4..." } ``` `proposal.cancelled`: ```json { "proposal_id": "spr_01H9X4M2P5R8T6V0", "reason": "organizer_cancelled" } ``` `reason` is `organizer_cancelled` or `all_declined`. `proposal.expired`: ```json { "proposal_id": "spr_01H9X4M2P5R8T6V0" } ``` ### Retry policy Failed deliveries are retried at **fixed offsets** — immediate → +1 min → +5 min → +30 min — for a total of **4 attempts**. After all 4 attempts fail, the delivery is marked `failed`. A subscription that accumulates **50 cumulative failed deliveries** is automatically disabled (`active: false`). Respond with any 2xx status within 10 seconds to acknowledge delivery. --- ## iCal Subscriptions Subscribe a Chronary calendar to an external iCal feed (e.g. Google Calendar, Outlook, Apple Calendar). The feed is polled approximately every 5 minutes and events are synced into the target calendar. iCal subscription ID prefix: `ics_` ### GET /v1/agents/{agent_id}/ical-subscriptions List iCal subscriptions for an agent. Query parameters: - `status` (optional): `active` | `error` | `paused` - `limit` (optional, default 50, max 200) - `offset` (optional, default 0) Response 200: ```json { "data": [ { "id": "ics_01H9X4M2P5R8T6V0", "agent_id": "agt_01H9X4M2P5R8T6V0", "calendar_id": "cal_01H9X4M2P5R8T6V0", "url": "https://calendar.google.com/calendar/ical/example/basic.ics", "label": "Google Calendar", "status": "active", "last_synced_at": "2025-01-15T10:00:00.000Z", "last_error": null, "created_at": "2025-01-15T10:00:00.000Z" } ], "total": 1 } ``` ### POST /v1/agents/{agent_id}/ical-subscriptions Subscribe an agent's calendar to an external iCal feed. The URL must be HTTPS. Request body: ```json { "calendar_id": "cal_01H9X4M2P5R8T6V0", "url": "https://calendar.google.com/calendar/ical/example/basic.ics", "label": "Google Calendar" } ``` Fields: - `calendar_id` (required): target Chronary calendar ID - `url` (required): HTTPS URL to the `.ics` file — must be publicly accessible - `label` (optional): human-readable name for the subscription Response 201: iCal subscription object ### GET /v1/ical-subscriptions/{id} Get a single iCal subscription by ID. Response 200: iCal subscription object ### PATCH /v1/ical-subscriptions/{id} Update an iCal subscription. At least one field required. Request body (all optional): ```json { "label": "Updated label", "url": "https://calendar.google.com/calendar/ical/new-feed/basic.ics" } ``` Response 200: Updated iCal subscription object ### DELETE /v1/ical-subscriptions/{id} Delete an iCal subscription. Does not delete events already synced. Response 204: No content --- ## iCal Feeds Every Chronary calendar has an iCal feed URL at `https://api.chronary.ai/ical/{ical_token}`. The token is returned in the `ical_token` field of the calendar object. ### GET /ical/{ical_token} Returns the calendar's events in iCalendar (RFC 5545) format. No authentication required — the token acts as a capability. Response 200: - Content-Type: `text/calendar` - Cache-Control: 5-minute public cache - ETag header for conditional requests Each event's resolved reminders are emitted as `VALARM` components inside its `VEVENT` (`ACTION:DISPLAY`, `TRIGGER:-PTM` relative to `DTSTART`), so subscribed calendar apps show the alarm. Events with no reminders emit no `VALARM`. --- ## Usage ### GET /v1/usage Returns current-month usage counters and plan limits for the authenticated organization. Response 200: ```json { "plan": "free", "period_start": "2026-04-01T00:00:00.000Z", "period_end": "2026-04-30T23:59:59.000Z", "agents": { "used": 3, "limit": 5 }, "calendars": { "used": 7, "limit": 10 }, "events": { "used": 1842, "limit": 5000 }, "api_calls": { "used": 12450, "limit": 50000 }, "webhooks": { "used": 956, "limit": 10000 }, "availability_queries": { "used": 304, "limit": 10000 }, "ical_subscriptions": { "used": 2, "limit": 3 }, "proposals": { "used": 18, "limit": 500 } } ``` Each metric is returned as `{ used, limit }` where `limit: null` means unlimited (Pro/Scale plans). `proposals` tracks scheduling-proposal creation this period. --- ## Audit Log ### GET /v1/audit-log Returns mutating API operations and auth lifecycle events for the calling organization. **Org-level API keys only** — agent-scoped keys receive `403`. Results ordered newest first. Retention is per-tier: Free = 3 days, Pro = 90 days, Custom = per contract. Query parameters: `from` (ISO-8601, inclusive lower bound), `to` (ISO-8601, defaults to now), `action` (exact action string, e.g. `agent.create`), `actor_key_prefix` (first 20 chars), `cursor` (opaque keyset cursor), `limit` (1–200, default 50). Response 200: ```json { "data": [ { "id": "aud_abc123def456", "action": "agent.create", "actor_key_prefix": "chr_sk_abcdef12345678", "agent_id": null, "resource": "", "ip": "203.0.113.10", "status": 201, "method": "POST", "path": "/v1/agents", "duration_ms": 42, "request_id": "req_8e9f...", "created_at": "2026-05-10T10:00:00.000Z" } ], "pagination": { "next_cursor": null }, "retention_days": 90, "range_clamped": false } ``` `range_clamped: true` means the requested `from` was older than the retention window and was silently clamped. `retention_days: null` = unlimited (Custom tier). When clamped on Free (3d), upgrade to Pro for 90-day history; when clamped on Pro (90d), Custom tier offers extended retention. --- ## Plans ### GET /v1/plans Returns the public plan catalog. **No authentication required.** Responses are cacheable at the edge (`Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400`) and carry an `ETag` for 304 conditional requests. IP-rate-limited. Returns three tiers: `free`, `pro`, and `custom`. Internal signup-state tiers (`free-agent`, `free-agent-unverified`) and the internal billing tier (`scale`) are not included. Response 200: ```json { "plans": [ { "id": "free", "name": "Free", "tagline": "For prototyping and small agents", "price": 0, "currency": "usd", "limits": { "agents": 3, "calendars": 10, "events": 2500, "api_calls": 50000, "webhook_deliveries": 5000, "availability_queries": 10000, "ical_subscriptions": 5, "proposals": 0, "webhook_endpoints": 3, "scoped_keys": 0 }, "display_features": ["3 agents", "10 calendars", "2,500 events/month", "50,000 API calls/month", "5,000 webhook deliveries", "5 iCal subscriptions", "3-day audit log history", "Community support"], "recommended": false }, { "id": "pro", "name": "Pro", "tagline": "For production agent workflows", "price": 2900, "currency": "usd", "limits": { "agents": 50, "calendars": 250, "events": 125000, "api_calls": 1000000, "webhook_deliveries": 250000, "availability_queries": 1000000, "ical_subscriptions": 100, "proposals": null, "webhook_endpoints": 25, "scoped_keys": 50 }, "display_features": ["Everything in Free, plus:", "Scheduling proposals & temporal holds", "Cross-agent availability", "50 per-agent API keys", "90-day audit log history", "50 agents", "250 calendars", "125,000 events/month", "1,000,000 API calls/month", "Email support"], "recommended": true }, { "id": "custom", "name": "Custom", "tagline": "For multi-agent platforms and enterprise", "price": null, "currency": null, "limits": null, "display_features": ["Custom volume limits", "Custom rate limits and webhook retries", "Custom audit log retention", "Dedicated support engineer", "Invoice billing", "Security reviews"], "recommended": false, "custom_pricing": true, "contact_url": "https://chronary.ai/contact" } ] } ``` Field notes: - `price` is an integer in the smallest currency unit (USD cents) — Stripe convention, never a float. `null` for custom-priced tiers. - `currency` is lowercase ISO-4217. `null` for custom-priced tiers. - `limits` is machine-readable. `null` *inside* the object (e.g. `proposals: null` on Pro) means unlimited for that metric; a `null` `limits` object (Custom) means caps are negotiated separately. - `display_features` is marketing copy — use `limits` for capability checks. - `custom_pricing: true` + `contact_url` appear on the Custom tier only. --- ## Agent Auth Agent self-signup lets an AI agent provision its own Chronary organization without a human in the loop, then upgrade to full live-mode access by verifying a one-time code sent to the signup email. The flow is two requests. ### POST /v1/agent/sign-up Unauthenticated. Body: ```json { "email": "alice@example.com", "agent_name": "Alice Bot", "tos_version": "2026-04-17" } ``` Rate-limited at 5 requests per 60 seconds per client IP, plus a domain-velocity limit of 10 signups per 60 minutes per normalized email domain. The exact `tos_version` string is required — fetch it from `GET /v1/auth/terms/current`. Response 200 — new org: ```json { "org_id": "org_abc123", "agent_id": "agt_abc123", "api_key": "chr_sk_restricted_abc...", "message": "Verification code sent to email" } ``` The `api_key` is **restricted** — it can issue GET requests for discovery and `POST /v1/agent/verify`, but every other write returns `403 forbidden` until the OTP is confirmed. Response 200 — existing-org dedup or domain-velocity block: ```json { "message": "Verification code sent to email" } ``` The same opaque message is returned whether or not an org already exists for the email, so `/v1/agent/sign-up` cannot be used to enumerate accounts. Response 409 `tos_version_stale`: ```json { "error": { "type": "tos_version_stale", "message": "The submitted terms-of-service version is out of date", "current_version": "2026-05-01", "request_id": "req_..." } } ``` ### POST /v1/agent/verify Authenticated with the restricted key returned by sign-up. Body: `{ "otp": "123456" }` (six numeric digits). Response 200: ```json { "verified": true, "message": "Full access unlocked" } ``` On success the org transitions `status: unverified → verified`, the plan upgrades from `free-agent-unverified` to `free-agent`, and the same `api_key` gains full write access — no key rotation needed. Response 400 `validation_error` is returned for every failure mode (wrong OTP, expired, burned after 10 attempts) — no information leak between those cases. Re-call `POST /v1/agent/sign-up` with the same email to get a fresh code. --- ## Feedback ### POST /v1/feedback Submit a structured feedback report (bug, feature request, or friction note). Fire-and-forget: no DB record, logged to Chronary's observability stack. Available on every plan including free. Rate-limited to **25 submissions per UTC day per organization**. The 26th submission returns `429` with a `Retry-After` header set to seconds until the next UTC midnight. Request: ```json { "type": "bug", "message": "Availability endpoint returns empty slots for UTC+13 timezones.", "context": { "sdk_name": "chronary-python", "sdk_version": "0.4.2", "endpoint": "GET /v1/availability" } } ``` - `type` (required): one of `bug`, `feature`, `friction`. - `message` (required): 10–2000 characters. - `context` (optional): arbitrary JSON object. Recommended fields: `sdk_name`, `sdk_version`, `endpoint`, `request_id`, `error_type`, `http_status`. Response 202: ```json { "status": "accepted" } ``` Response 429 (daily cap reached): ```json { "error": { "type": "quota_exceeded", "message": "Daily feedback limit of 25 reached. Try again tomorrow.", "request_id": "req_..." } } ``` `agent_id` is sourced from the auth context — do not pass it in the body. --- ## Account ### GET /v1/auth/export Returns a JSON dump of every row this org owns (GDPR Art. 15 right of access + Art. 20 portability, CCPA right-to-know, EU Data Act interoperability). **JWT-authenticated** — API keys (`chr_sk_*` / `chr_ak_*`) return 401, because the payload contains decrypted webhook secrets and iCal subscription URLs that aren't normally exposed to API-key callers. **Rate limit:** 10 exports/hour/org. **Response headers:** `Content-Disposition: attachment; filename="chronary-export-YYYY-MM-DD.json"` and `Cache-Control: no-store`. **Included** (your data): org metadata, agents, calendars, events with decrypted titles + descriptions, availability rules, iCal subscriptions with decrypted URL, webhook subscriptions with decrypted secret + URL, scheduling proposals + slots + responses, API key prefixes + labels (no key, no hash), usage records, quota counters, ToS acceptance audit rows, account claims this org initiated. **Omitted** (not your data, or unrecoverable): password hashes, OTP hashes, claim revocation tokens, API key hashes (irrecoverable), internal scheduling state (`started_scheduled_for`, `hold_expiry_scheduled_for`, etc.), incident records (Chronary's infra audit log, not user data), claims targeting this org (would expose third-party identity). iCal portability is satisfied alongside the JSON dump: every calendar entry includes its `ical_token` and a public `ical_feed_url` you can subscribe to from any RFC 5545 client. In day-to-day use, you'll trigger this from the console at `console.chronary.ai/settings`. The HTTP endpoint exists for programmatic compliance tooling holding a delegated JWT. Errors: `401 authentication_error` (missing/invalid JWT or any API key), `429 rate_limited` (more than 10/hour for this org). ### DELETE /v1/auth/account Hard-deletes the authenticated org. Cascade-deletes agents, API keys, calendars, events, iCal subscriptions, webhook subscriptions (and deliveries), scheduling proposals + slots + responses, availability rules, usage records, quota counters, and account-claims initiated by this org. Retains `tos_acceptances` rows with NULL `org_id` per Washington RCW 4.16.040 (6-year contract statute of limitations / GDPR Art. 17(3)(e) legal-obligation exception). Also retains `incident_records` (audit log) and `account_claims` rows targeting this org (state machine). Clears the session cookie and returns `204 No Content`. There is no recovery path. JWT-authenticated. --- ## Terms of Service ### GET /v1/auth/terms/current Returns the current ToS manifest entry. **No authentication required** — intended for CLIs, SDKs, and agent-signup flows that need to read the version at runtime rather than rely on a build-time constant. Response 200: ```json { "version": "2026-04-26", "effective_at": "2026-04-26", "url": "https://chronary.ai/terms/v/2026-04-26", "material": true } ``` - `version` is opaque — pass verbatim as `tos_version` on signup and re-acceptance. - `material: true` indicates existing customers must re-accept to continue. ### PATCH /v1/auth/terms Appends an immutable row to the `tos_acceptances` audit table (`source = 'reaccept'`) capturing the accepting org, client IP, user-agent, and document SHA-256, then updates the denormalized `accepted_terms_version` / `accepted_terms_at` columns on the organization. Prior acceptance rows are preserved. **Authentication:** JWT (console session cookie or Bearer token). Not an API key — this endpoint is not invokable with `chr_sk_*` / `chr_ak_*` credentials. Request: ```json { "tos_version": "2026-04-26" } ``` Response 200: ```json { "data": { "accepted_terms_version": "2026-04-26", "accepted_terms_at": "2026-04-26T20:30:00.000Z" } } ``` Errors: - `400 validation_error` — missing or empty `tos_version`. - `401 authentication_error` — no valid JWT. - `409 tos_version_stale` — submitted version does not match `CURRENT_TERMS_VERSION`. Response body includes `current_version`. ### Staleness signaling - **`Chronary-Terms-Upgrade-Required` response header.** Set on authenticated REST + MCP responses when the current ToS version is `material: true` and the authenticated org's `accepted_terms_version` is stale. Non-blocking — the header advertises the new version; agents continue operating. The console uses this header (and the `accepted_terms_version` on `GET /v1/auth/me`) to mount a blocking re-acceptance modal. - **`409 tos_version_stale`.** Returned by every write path that accepts a `tos_version` input — email signup, OAuth initiate, agent signup, and `PATCH /v1/auth/terms`. Body shape: ```json { "error": { "type": "tos_version_stale", "message": "The submitted terms-of-service version is out of date", "current_version": "2026-04-26", "request_id": "req_..." } } ``` Refresh via `GET /v1/auth/terms/current`, re-render the disclosure, and retry. --- ## Rate-Limit Headers Every authenticated response emits two headers per [draft-ietf-httpapi-ratelimit-headers-09](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-09): ``` RateLimit-Policy: "quota";q=1000000;w=2592000;pk=org_abc123 RateLimit: "quota";r=985477;t=187200 ``` - `q`: monthly quota limit - `w`: window size in seconds (~30 days) - `pk`: partition key (your org ID) - `r`: requests remaining - `t`: delta-seconds until reset (clock-skew safe — not epoch) On 429 responses, `Retry-After` carries the same delta-seconds value. Public endpoints (`/v1/plans`, `/v1/auth/*`, `/v1/agent/sign-up`) do not emit these headers. --- ## MCP Server Chronary also exposes an MCP (Model Context Protocol) server at `https://api.chronary.ai/mcp`. This allows Claude, Cursor, and other MCP hosts to use Chronary tools directly without REST API calls. The MCP server uses Streamable HTTP transport. Authentication uses the same API keys as the REST API. Available tools (19): - **Agents:** `create_agent`, `list_agents` - **Calendars:** `create_calendar`, `get_calendar_context` - **Events:** `create_event`, `list_events`, `cancel_event`, `confirm_event`, `release_event` - **Availability:** `get_availability`, `find_meeting_time` - **Availability rules:** `set_availability_rules`, `get_availability_rules`, `clear_availability_rules` - **Scheduling proposals** (Pro plan, org-key only): `create_proposal`, `respond_to_proposal`, `resolve_proposal`, `cancel_proposal` - **iCal sync:** `subscribe_ical` See [MCP documentation](https://docs.chronary.ai/mcp/overview/) for setup and tool details. --- ## TypeScript SDK ```bash npm install @chronary/sdk ``` Package page: https://www.npmjs.com/package/@chronary/sdk — published from `Chronary/chronary-node` with npm OIDC trusted publishing and Sigstore provenance attestations. ```ts import { Chronary } from '@chronary/sdk'; const client = new Chronary({ apiKey: 'chr_sk_live_...' }); // Register your agent const agent = await client.agents.create({ name: 'My Agent', type: 'ai' }); // Create a calendar (agentId is optional — omit for org-level) const calendar = await client.calendars.create({ agentId: agent.id, name: 'Work Calendar', timezone: 'America/New_York', }); // Create an event (calendarId is positional) const event = await client.events.create(calendar.id, { title: 'Team standup', start_time: '2026-04-07T09:00:00Z', end_time: '2026-04-07T09:30:00Z', }); ``` --- ## Python SDK ```python pip install chronary ``` ```python from chronary import Chronary client = Chronary(api_key="chr_sk_live_...") # Register your agent agent = client.agents.create(name="My Agent", type="ai") # Create a calendar calendar = client.calendars.create( agent_id=agent.id, name="Work Calendar", timezone="America/New_York" ) # Create an event event = client.events.create( calendar_id=calendar.id, title="Team standup", start_time="2025-01-20T09:00:00Z", end_time="2025-01-20T09:30:00Z" ) # Check availability slots = client.availability.get( agent_id=agent.id, start="2025-01-20T00:00:00Z", end="2025-01-21T00:00:00Z", slot_duration="30m" ) ``` See [Python Quickstart](https://docs.chronary.ai/getting-started/python-quickstart/) for full documentation. --- ## Agent Framework Adapters ```bash npm install @chronary/toolkit ``` Package page: https://www.npmjs.com/package/@chronary/toolkit — published from `Chronary/chronary-toolkit` with npm OIDC trusted publishing and Sigstore provenance attestations. Drop Chronary's 23 calendar tools into the agent framework you already use. One package, same tool surface, separate entry point per framework: ```ts // Vercel AI SDK import { tools } from '@chronary/toolkit/ai-sdk'; const result = await generateText({ model, tools: tools({ apiKey }) }); // OpenAI Agents import { tools } from '@chronary/toolkit/openai'; // LangChain import { tools } from '@chronary/toolkit/langchain'; // Mastra import { tools } from '@chronary/toolkit/mastra'; // MCP (build your own server) import { server } from '@chronary/toolkit/mcp'; ``` Optional peer deps — install only the frameworks you use. Tool annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`) surface to hosts so users get confirmation prompts on destructive actions. --- ## Webhook + Event Schemas ```bash npm install @chronary/schemas ``` Package page: https://www.npmjs.com/package/@chronary/schemas — published from `Chronary/chronary-schemas` with npm OIDC trusted publishing and Sigstore provenance attestations. Standalone Zod schemas for typed webhook payload validation — useful when you're handling Chronary webhooks without the full SDK: ```ts import { parseWebhookEvent } from '@chronary/schemas/events'; const event = parseWebhookEvent(JSON.parse(rawBody)); // event is fully typed; throws ZodError on shape mismatch switch (event.type) { case 'event.created': /* event.data.event is typed */ break; case 'proposal.confirmed': /* event.data.proposal is typed */ break; } ``` Covers all 17 webhook event types. Zero non-Zod runtime dependencies. --- ## MCP Server (stdio, npm-installed) ```bash npx -y @chronary/mcp ``` Package page: https://www.npmjs.com/package/@chronary/mcp — published from `Chronary/chronary-mcp` with npm OIDC trusted publishing and Sigstore provenance attestations. Local stdio MCP server for hosts that don't speak HTTP/SSE (Claude Desktop, Cursor, VS Code Copilot, Windsurf, Claude Code). Same 23 tools as the hosted `https://api.chronary.ai/mcp` endpoint — pick whichever transport your host supports. Claude Desktop / Cursor config: ```json { "mcpServers": { "chronary": { "command": "npx", "args": ["-y", "@chronary/mcp"], "env": { "CHRONARY_API_KEY": "chr_sk_..." } } } } ``` Reduce context with `--tools ` to whitelist only the tools you need. See the [package README](https://www.npmjs.com/package/@chronary/mcp) for per-editor config snippets (incl. the Windows `cmd /c` wrapper) and the `--base-url` flag for self-hosted Chronary instances. --- ## CLI ``` npm install -g @chronary/cli chronary auth login chronary agents list chronary events create --calendar cal_abc --title "Meeting" --start "2025-01-20T09:00:00Z" --end "2025-01-20T09:30:00Z" chronary availability get --agent agt_abc --start "2025-01-20T00:00:00Z" --end "2025-01-21T00:00:00Z" ``` See [CLI Quickstart](https://docs.chronary.ai/getting-started/cli-quickstart/) for full documentation.