Events
Events are time-bound entries within a calendar. They support statuses, metadata, and trigger webhooks on changes.
Create an event
Section titled “Create an event”POST /v1/calendars/:cal_id/eventsRequest body
Section titled “Request body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | Yes | — | 1–500 characters |
start_time | string | Yes | — | ISO 8601 datetime (UTC) |
end_time | string | Yes | — | ISO 8601 datetime (must be after start_time) |
description | string | No | — | Free-text description |
all_day | boolean | No | false | Mark as all-day event |
status | string | No | confirmed | confirmed, tentative, cancelled, or hold |
metadata | object | No | — | Key-value pairs, max 16KB JSON |
reminders | integer[] | null | No | inherit | Minutes before start_time to fire an event.reminder webhook (e.g. [10, 1440] = 10 min and 1 day before). Max 5 entries, each 1–40320 (28 days). null inherits the calendar’s default_reminders; [] means no reminders. See Reminders. |
recurrence_rule | string | No | — | RFC 5545 RRULE subset (without the RRULE: prefix) that makes this event a recurring series, e.g. FREQ=WEEKLY;BYDAY=MO,WE;COUNT=12. Not allowed with status=hold. See Recurring events. |
hold_expires_at | string | Conditional | — | Required when status=hold. ISO 8601 datetime 30s–15min in the future. |
hold_priority | integer | No | 0 | Only valid with status=hold. 0–100. Higher-priority holds pre-empt lower. |
Example
Section titled “Example”curl -X POST https://api.chronary.ai/v1/calendars/cal_x1y2z3/events \ -H "Authorization: Bearer chr_sk_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "title": "Strategy sync with Acme Corp", "start_time": "2026-04-07T14:00:00Z", "end_time": "2026-04-07T14:30:00Z", "description": "Quarterly strategy alignment", "metadata": { "deal_id": "deal_789" } }'from chronary import Chronary
client = Chronary()
event = client.events.create( calendar_id="cal_x1y2z3", title="Strategy sync with Acme Corp", start_time="2026-04-07T14:00:00Z", end_time="2026-04-07T14:30:00Z", description="Quarterly strategy alignment", metadata={"deal_id": "deal_789"},)Response 201 Created
Section titled “Response 201 Created”{ "id": "evt_m1n2o3", "calendarId": "cal_x1y2z3", "orgId": "org_a1b2c3", "title": "Strategy sync with Acme Corp", "description": "Quarterly strategy alignment", "startTime": "2026-04-07T14:00:00Z", "endTime": "2026-04-07T14:30:00Z", "allDay": false, "status": "confirmed", "source": "internal", "metadata": { "deal_id": "deal_789" }, "reminders": null, "holdExpiresAt": null, "holdPriority": 0, "recurrenceRule": null, "recurrenceExdates": [], "deletedAt": null, "createdAt": "2026-04-04T12:00:00Z", "updatedAt": "2026-04-04T12:00:00Z"}reminders is null when the event inherits its calendar’s default_reminders.
Triggers an event.created webhook.
Errors
Section titled “Errors”| Status | Type | Cause |
|---|---|---|
400 | bad_request | Invalid recurrence_rule (unsupported key, COUNT and UNTIL together, start_time doesn’t match the rule pattern, …) |
403 | plan_required | Free-plan recurrence_rule is unbounded or ends more than 90 days after the series start (feature: "unbounded_recurrence") |
404 | not_found | Calendar not found |
429 | quota_exceeded | Active recurring-series limit for your plan reached |
List events
Section titled “List events”GET /v1/calendars/:cal_id/eventsQuery parameters
Section titled “Query parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
start_after | datetime | — | Events starting after this time |
start_before | datetime | — | Events starting before this time |
status | string | — | confirmed, tentative, cancelled, or hold |
source | string | — | internal or external_ical |
limit | integer | 50 | 1–200 |
offset | integer | 0 | Pagination offset |
expand | boolean | false | Expand recurring series into per-occurrence instances. Requires both start_after and start_before (window at most 366 days). See Expanding occurrences. |
Example
Section titled “Example”curl "https://api.chronary.ai/v1/calendars/cal_x1y2z3/events?start_after=2026-04-01T00:00:00Z&start_before=2026-04-30T23:59:59Z&status=confirmed" \ -H "Authorization: Bearer chr_sk_your_key_here"for event in client.events.list( calendar_id="cal_x1y2z3", start_after="2026-04-01T00:00:00Z", start_before="2026-04-30T23:59:59Z", status="confirmed",): print(event.title)Response 200 OK
Section titled “Response 200 OK”{ "data": [ ... ], "total": 15, "limit": 50, "offset": 0}With expand=true, total is the number of items in the requested window (masters expanded into instances) rather than the number of stored events.
Errors
Section titled “Errors”| Status | Type | Cause |
|---|---|---|
400 | validation_error | expand=true without both time bounds, or a window wider than 366 days |
404 | not_found | Calendar not found |
List events by agent
Section titled “List events by agent”Returns events across all calendars owned by an agent.
GET /v1/agents/:agent_id/eventsSame query parameters as calendar-scoped listing.
Example
Section titled “Example”curl "https://api.chronary.ai/v1/agents/agt_a1b2c3d4/events?start_after=2026-04-07T00:00:00Z" \ -H "Authorization: Bearer chr_sk_your_key_here"for event in client.agents.events.list( "agt_a1b2c3d4", start_after="2026-04-07T00:00:00Z",): print(f"{event.title} ({event.calendar_id})")Get an event
Section titled “Get an event”GET /v1/calendars/:cal_id/events/:idErrors
Section titled “Errors”| Status | Type | Cause |
|---|---|---|
404 | not_found | Event not found |
Update an event
Section titled “Update an event”PATCH /v1/calendars/:cal_id/events/:idRequest body
Section titled “Request body”| Field | Type | Description |
|---|---|---|
title | string | 1–500 characters |
description | string | null | Set to null to clear |
start_time | datetime | ISO 8601 |
end_time | datetime | ISO 8601 |
all_day | boolean | — |
status | string | confirmed, tentative, or cancelled |
metadata | object | Replaces existing metadata |
reminders | integer[] | null | Replaces the event’s reminders. null reverts to inheriting the calendar’s default_reminders; [] disables reminders. See Reminders. |
recurrence_rule | string | null | Sets or changes the series rule — a full-series edit. null converts the series back to a one-off event. Changing the rule or start_time clears any individually cancelled occurrences. See Recurring events. |
At least one field must be provided. Triggers an event.updated webhook.
Errors
Section titled “Errors”| Status | Type | Cause |
|---|---|---|
400 | bad_request | Invalid recurrence_rule, or the (new) start_time doesn’t match the rule pattern |
403 | forbidden | External iCal events are read-only |
403 | plan_required | Free-plan recurrence_rule is unbounded or ends more than 90 days after the series start |
404 | not_found | Event or calendar not found |
429 | quota_exceeded | Adding a rule to an existing event would exceed the active recurring-series limit |
Delete an event
Section titled “Delete an event”DELETE /v1/calendars/:cal_id/events/:idReturns 204 No Content. Triggers an event.deleted webhook. On a recurring series, a bare DELETE cancels the whole series.
Query parameters
Section titled “Query parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
occurrence_start | datetime | — | Recurring series only: cancel just the occurrence starting at this ISO 8601 time. The rest of the series is unaffected. |
With occurrence_start, the response is 200 OK with the updated series master (the cancelled start is appended to recurrenceExdates) and an event.updated webhook fires instead of event.deleted. See Cancelling occurrences.
Errors
Section titled “Errors”| Status | Type | Cause |
|---|---|---|
400 | bad_request | occurrence_start is not an active occurrence of this series |
403 | forbidden | External iCal events are read-only |
404 | not_found | Event or calendar not found |
409 | conflict | occurrence_start provided but the event is not a recurring series |
Reminders
Section titled “Reminders”Reminders are timed notifications fired ahead of an event’s start_time. Each entry is an integer number of minutes before start — e.g. [10, 1440] fires 10 minutes and 1 day before. An array may hold up to 5 entries, each between 1 and 40320 (28 days).
Resolution
Section titled “Resolution”Reminders resolve in precedence order:
- The event’s own
remindersfield, if set to an array. - The calendar’s
default_reminders, if the event’sremindersisnull. - The system default of
[10](10 minutes before) if neither is set.
A null value means “inherit from the level above”; an empty array [] means “explicitly no reminders” and stops resolution — it does not fall through to the default.
Delivery
Section titled “Delivery”Each resolved reminder fires an event.reminder webhook at start_time minus the offset. Reminders fire only for events with status: "confirmed" — tentative events, holds, and cancelled events do not fire reminders. Reminders are also emitted as VALARM components in the iCal feed so subscribed calendar apps display the alarm.
Recurring events
Section titled “Recurring events”Pass recurrence_rule when creating an event to turn it into a recurring series. One event row is the series — it counts once toward your monthly events quota no matter how many times it repeats, and once toward your plan’s active recurring-series cap.
Rule format
Section titled “Rule format”recurrence_rule is an RFC 5545 RRULE subset without the RRULE: prefix, e.g. FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12:
| Part | Values | Notes |
|---|---|---|
FREQ | DAILY, WEEKLY, MONTHLY, YEARLY | Required |
INTERVAL | 1–99 | Optional, default 1 (e.g. INTERVAL=2 with FREQ=WEEKLY = every other week) |
COUNT | 1–730 | Total number of occurrences. Mutually exclusive with UNTIL. |
UNTIL | YYYYMMDDTHHMMSSZ or YYYYMMDD | Last possible occurrence start (inclusive), UTC basic format. Mutually exclusive with COUNT. |
BYDAY | Weekly: list like MO,WE,FR · Monthly: single ordinal like 2TU or -1FR | Weekly and monthly only |
BYMONTHDAY | 1–28 or -1 (last day of the month) | Monthly only; mutually exclusive with BYDAY |
Rules the engine cannot expand are rejected with 400 bad_request. Two behaviors to know:
- Skipped, never clamped — occurrences that don’t exist in a month (day 29–31, Feb 29 on
YEARLY) are skipped, which is whyBYMONTHDAYstops at 28 (use-1for “last day”). start_timemust match the rule — the series master’sstart_timeis always the first occurrence, so aFREQ=WEEKLY;BYDAY=MOrule requires astart_timethat falls on a Monday.
recurrence_rule is not allowed on holds.
Plan limits
Section titled “Plan limits”| Plan | Active series | Rule bounds |
|---|---|---|
| Free | 5 | Must be bounded (COUNT or UNTIL) and end within 90 days of the series start |
| Pro | 250 | Unbounded rules allowed |
| Custom | Custom | Custom |
Exceeding the active-series cap returns 429 quota_exceeded; an unbounded (or over-window) rule on the Free plan returns 403 plan_required with feature: "unbounded_recurrence". Deleting a series frees its slot. Your current count is reported by GET /v1/usage as recurring_events.
Expanding occurrences
Section titled “Expanding occurrences”List endpoints return the series master as a single event by default. To see individual occurrences, pass expand=true with both start_after and start_before (window at most 366 days):
curl "https://api.chronary.ai/v1/calendars/cal_x1y2z3/events?expand=true&start_after=2026-04-01T00:00:00Z&start_before=2026-04-30T23:59:59Z" \ -H "Authorization: Bearer chr_sk_your_key_here"Expanded instances share the master’s id and carry two extra fields:
{ "id": "evt_m1n2o3", "title": "Weekly sync", "startTime": "2026-04-13T09:00:00Z", "endTime": "2026-04-13T09:30:00Z", "recurrenceRule": "FREQ=WEEKLY;BYDAY=MO;COUNT=12", "recurringEventId": "evt_m1n2o3", "originalStartTime": "2026-04-13T09:00:00Z"}| Field | Description |
|---|---|
recurringEventId | The series master’s id (present only on expanded instances) |
originalStartTime | The occurrence start this instance was generated for — pass it to occurrence_start to cancel that occurrence |
Expansion also works on GET /v1/agents/:agent_id/events. Availability queries always account for recurring occurrences — each one blocks slots like a regular event.
Editing a series
Section titled “Editing a series”PATCH operates on the whole series: changing title, times, or the rule applies to every occurrence. Set recurrence_rule: null to convert the series back to a one-off event. Changing the rule or start_time resets any individually cancelled occurrences (clears recurrenceExdates).
Cancelling occurrences
Section titled “Cancelling occurrences”- One occurrence —
DELETE .../events/:id?occurrence_start=2026-04-13T09:00:00Zreturns200 OKwith the updated master; the series lives on and anevent.updatedwebhook fires. - Whole series — bare
DELETE .../events/:idreturns204 No Contentand firesevent.deleted.
To modify a single occurrence (different time or title for one week), cancel that occurrence and create a standalone event in its place:
# 1. Cancel the April 13 occurrencecurl -X DELETE "https://api.chronary.ai/v1/calendars/cal_x1y2z3/events/evt_m1n2o3?occurrence_start=2026-04-13T09:00:00Z" \ -H "Authorization: Bearer chr_sk_your_key_here"
# 2. Create a one-off replacementcurl -X POST https://api.chronary.ai/v1/calendars/cal_x1y2z3/events \ -H "Authorization: Bearer chr_sk_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "title": "Weekly sync (moved)", "start_time": "2026-04-13T14:00:00Z", "end_time": "2026-04-13T14:30:00Z" }'Webhooks and feeds
Section titled “Webhooks and feeds”Creating, updating, and deleting a series fires the regular event.created / event.updated / event.deleted webhooks for the master. Cancelling a single occurrence (?occurrence_start=) fires event.updated.
Each occurrence also fires its own event.started, event.ended, and event.reminder lifecycle webhooks at the occurrence’s times. Those payloads carry occurrence_start (the ISO start of the occurrence), and their start_time / end_time are the occurrence’s own times while event_id remains the series master’s id. Occurrences cancelled via occurrence_start do not fire.
In the iCal feed, a series is emitted as a single VEVENT with RRULE (and EXDATE for cancelled occurrences) — calendar apps expand it client-side.
Temporal holds
Section titled “Temporal holds”Holds are tentative reservations that auto-release after a short TTL (max 15 minutes). Use them to eliminate race conditions when multiple agents compete for the same slot: place a hold first, decide second.
A hold behaves like a normal event for availability purposes — it blocks the slot against queries and other holds — but auto-expires if not confirmed.
Lifecycle
Section titled “Lifecycle”- Create a hold —
POST /v1/calendars/:cal_id/eventswithstatus: "hold"andhold_expires_at(30s–15min in the future). Firesevent.hold_created. - Confirm —
PUT /v1/events/:id/confirmpromotes it tostatus: "confirmed". Firesevent.hold_confirmed.event.started/event.endedlifecycle webhooks fire at the scheduled times. - Release —
PUT /v1/events/:id/releasemanually releases the hold. Firesevent.hold_released. - Auto-expire — if not confirmed or released by
hold_expires_at, the hold is cancelled andevent.hold_expiredfires.
Priority bumping
Section titled “Priority bumping”When creating a hold that overlaps an existing hold on the same calendar:
- Higher priority wins. If the new hold’s
hold_priorityis strictly greater than the existing one’s, the existing hold is cancelled (event.hold_expiredfires) and the new hold is created. - Equal or lower priority → 409
conflict. The new hold is rejected with errorhold_conflict.
A single create request with priority bumping therefore fires two webhooks in order: event.hold_expired (for the bumped hold), then event.hold_created (for the new one).
Confirm a hold
Section titled “Confirm a hold”PUT /v1/events/:id/confirmcurl -X PUT https://api.chronary.ai/v1/events/evt_m1n2o3/confirm \ -H "Authorization: Bearer chr_sk_your_key_here"event = client.events.confirm("evt_m1n2o3")Returns the confirmed event (200 OK). Fires event.hold_confirmed.
Errors
Section titled “Errors”| Status | Type | Cause |
|---|---|---|
403 | forbidden | External iCal events cannot be confirmed |
404 | not_found | Event not found |
409 | conflict | Event is not a hold (not_a_hold), or the hold has already expired (hold_expired) |
Release a hold
Section titled “Release a hold”PUT /v1/events/:id/releasecurl -X PUT https://api.chronary.ai/v1/events/evt_m1n2o3/release \ -H "Authorization: Bearer chr_sk_your_key_here"event = client.events.release("evt_m1n2o3")Returns the released (now cancelled) event (200 OK). Fires event.hold_released.
Errors
Section titled “Errors”| Status | Type | Cause |
|---|---|---|
403 | forbidden | External iCal events cannot be released |
404 | not_found | Event not found |
409 | conflict | Event is not a hold |
Constraints
Section titled “Constraints”PATCHon a hold is rejected with400 invalid_transition— use/confirm,/release, or wait for TTL.PATCHcannot setstatus=holdon a non-hold event.hold_expires_atis write-once at create time.- Holds count against your
eventsquota (same as regular event creation).