Recurring Events
Calendar is in private beta in US production (api.agentmail.to). The beta is
not available in EU production (api.agentmail.eu). Organizations without
access receive a 403 with the message Calendar access is not enabled for this organization. Email support@agentmail.to
to request access. Upgrade to the latest Python or TypeScript SDK to use the
calendar methods.
A recurring event is one series plus the dates it generates. You create and change the series by its UUID, and address each date by a dated ID.
Create a recurring event
Add recurrence to a normal create request. start and end describe the first date; the rule generates the rest.
Each date happens at 09:00 New York time, before and after the November daylight-saving change. Recurring timed events default to duration_mode: nominal, so each date also keeps its 09:15 end.
start must itself be a date the rule produces. Starting the example above on a Tuesday returns 400 with DTSTART must match recurrence.rule; it does not satisfy BYDAY.
Writing rules
recurrence.rule is an RFC 5545 RRULE, with or without the RRULE: prefix.
Supported parts are FREQ, INTERVAL, COUNT, UNTIL, WKST, BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR, BYMINUTE, BYSECOND and BYSETPOS. COUNT and UNTIL cannot be combined. Timed events require a UTC date-time for UNTIL (20261231T235959Z); all-day events require a date (20261231). The cutoff is inclusive.
The stored rule is normalized, so the rule you read back can differ textually from the one you sent while meaning the same thing.
Skipping and adding dates
exdates removes dates the rule would produce, and rdates adds dates it would not. Use the same format as start: date-times for timed events, dates for all-day events.
An exdates entry must match the date’s original start exactly. For timed events, an rdates entry can also be a period, such as {"start": "2026-11-28T09:00:00", "duration": "PT30M"}.
To skip a single date later, cancel that date; it adds the date to exdates for you. Cancelled dates count toward the 366 exdates limit, and cancelling a date beyond it returns 400 validation_error without changing the event or charging invitation emails. To skip dates on a regular pattern, change the rule instead.
Limits
A rule with no COUNT or UNTIL repeats indefinitely, which is fine.
Dated IDs
Each date of a series has its own ID: the series UUID, an underscore, and the date’s original start.
The time is the original wall-clock start in the series’ time zone, as the rule generated it. The ID stays the same after you move the date, so you can safely store it. Read it from event_id on agenda items, or build it yourself from a known date.
A dated event has kind: instance and three extra fields:
series_id: the series UUID.original_startandoriginal_start_at: when the rule placed the date, before any edit.is_exception:trueonce the date has been edited.
List the dates
There are two ways to list dates:
GET /calendar/agendalists every date of every event in a window. It includes recurring events’ dates only up to about 90 days from now.GET /calendar/events/{series_id}/instanceslists the dates of one series in a window. It computes them from the rule, so it works for any window up to 366 days, including far in the future.
Both lists apply edits to each date and leave out cancelled dates. Get a date by its dated ID for its full details, including description and attendees.
Change one date
PATCH a dated ID to change only that date. To make the change conditional, send the date’s own etag (from Get Event, which looks like "occ-0-0-0-none-none") in If-Match; dated events have no resource_revision.
You can change title, description, location, metadata, status, start, end, duration_mode and attendees for a date. Unlike a series or one-off event, a date accepts end without start. all_day, timezone and recurrence belong to the series and are rejected for a date.
An edited date keeps the fields you changed for it. Its other fields keep following the series, so renaming the series later also renames the moved date. The first edit to a date sends calendar.event.created for that date; later edits send calendar.event.updated.
The same timing rules as one-off events apply to each date: once a date has started its start is fixed, and once it has ended only its descriptive fields can change. See Events that have started or ended.
Change this date and every later date
Add mode=future to apply a change from one date onward, for example to rename a meeting from next week or make it longer:
A mode=future change cannot include start; it returns 409 future_edit_start_move_not_supported. To move the time of every remaining date, update the series itself. Single-date and future edits can coexist at the same date. Fields explicitly changed with mode=single keep their single-date values; other fields follow the applicable future edit.
Cancel dates
DELETE a dated ID to cancel dates without deleting the series:
mode=single(default) cancels that one date by adding its original start to the series’recurrence.exdates. The response shows the date withstatus: cancelled; afterwards it no longer appears in lists and gets no start or end webhooks. A date that is already running still gets itscalendar.event.ending. The date is now removed from the series, so reading its dated ID can return404or410. A date within about 90 days reads back ascancelledonce the series is regenerated, for 30 days after the cancel. To confirm a cancel, check the series’recurrence.exdates.mode=futurecancels that date and every later one, ending the series. The series’recurrence.truncate_beforerecords where it now stops.mode=futurefrom the first date deletes the whole series and returns adeletion_id, like deleting the series UUID.
Each sends calendar.event.updated with the cancelled date (deleting the whole series from its first date sends calendar.event.deleted). Add send_invites=true to email the cancellation to attendees.
Change the whole series
PATCH the series UUID to change every date. A change to the schedule (start, end, timezone, duration_mode, recurrence or status) regenerates the series’ dates, and start and end webhooks follow the new schedule. A date that is already in progress still gets its calendar.event.ending.
While dates are regenerated, which takes a few seconds, the agenda view can briefly leave out some of the series’ dates. Dates that have already started or ended keep reading back as they ran, in the agenda and by their dated ID, even if the new schedule no longer produces them. Changing such a date returns 410 event_expired.
To turn a series into a one-off event, set recurrence to null. This only works while none of its dates has been edited; otherwise it returns 409 event_in_progress, and you should create a new one-off event and delete the series instead.
Time zones and daylight saving
Rules are evaluated in wall-clock time in the series’ timezone, so each date keeps its local start time when clocks change. When a date’s local start falls in a gap (for example 02:30 on the night clocks spring forward), the date still occurs, moved later by the length of the gap: 02:30 becomes 03:30. Its dated ID and original_start keep 02:30. When the local start repeats (when clocks fall back), the earlier of the two times is used.
duration_mode decides the end of a date that spans a change: nominal (the default for series) keeps the local end time, and exact keeps the elapsed length.
