Skip to navigation

Concurrency, Limits & Errors

Beta
ETags, idempotency, limits and error codes for the calendar API.

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.

ETags and If-Match

Every calendar, event and date has an etag that changes whenever it does. Get, create, update and respond responses, and deletes that cancel dates, return it in the body as etag and in the ETag header. Deleting a one-off or series event returns only a deletion_id.

If-Match is optional on Update Calendar, Update Event, Delete Event and Respond to Event:

  • Without it, the change applies to the resource as it is when the request arrives. If another change lands while your request is running, you get 409 race_condition; retry.
  • With it, the change applies only if the resource still has that etag. Otherwise you get 412 precondition_failed and nothing changes.

Send If-Match when your change depends on what you read, for example when you replace attendees with a list built from the current one, so you don’t overwrite a response that arrived in the meantime. Take the etag from a read with consistency=primary (or from your last write), since a regional read can be a few seconds old.

Resourceetag looks like
Calendar"rv-3"
One-off or series event (UUID)"rv-5", which is "rv-" + resource_revision
Date of a series (dated ID)"occ-5-2-1-none-none"; treat it as opaque

Changing one date changes the etag of every date of that series, and so does changing the series. Use the etag of the exact thing you are changing, from your latest read or write. Weak validators (W/"rv-5"), which some proxies produce, are accepted. If-Match: * matches any current version, the same as sending no If-Match.

Handling 412

A 412 precondition_failed body includes current_revision. Read the resource again, decide whether your change still makes sense, and retry with the new etag:

from agentmail.core.api_error import ApiError
def reschedule(client, inbox_id, event_id, start, end, attempts=3):
for _ in range(attempts):
event = client.inboxes.calendar.get_event(inbox_id, event_id, consistency="primary")
try:
return client.inboxes.calendar.update_event(
inbox_id, event_id, if_match=event.etag, start=start, end=end
)
except ApiError as error:
if error.status_code != 412:
raise
raise RuntimeError("event kept changing; giving up")

Read with consistency="primary" before retrying, so you do not get a slightly stale copy and fail again.

Retrying safely

OperationHow to make retries safe
Create EventSend a client_id. A retry with the same client_id and body returns the original event with 200, for as long as the event exists, and is not charged against your send limits again.
Delete a one-off or series eventA retry returns the same deletion_id while removal runs, and 404 after it has finished. Send the same Idempotency-Key each time, or none and the same send_invites; a keyless retry that changes send_invites is a different delete and gets 404 while removal runs, without sending anything.
Update Event, RespondWithout If-Match, a retry applies the same values again; that is safe for values like start or title, but sends another calendar.event.updated (calendar.event.responded for Respond). With send_invites (send_reply for Respond), it also emails again and is charged against your send limits again. With If-Match, a retry after a successful first attempt gets 412 because the version moved; read the event to confirm your change is there.
Cancel a dateIf your first attempt succeeded, the retry gets 404 or 410 (or 412 with If-Match), because the date has left the series. Get the series and check that recurrence.exdates (or, for mode=future, recurrence.truncate_before) covers the date. A mode=future delete from the first date deletes the whole series, so there the series also returns 404, which means the delete succeeded.
Update CalendarAs for Update Event.

Limits

LimitValue
Events per calendar10,000 one-off and recurring events by default
Request body128 KB for events, 8 KB for calendar settings
title1 to 1,000 characters
description65,536 bytes
location1,024 characters
metadata16,384 bytes, serialized as JSON
attendees100 per event
Attendee name / comment256 characters / 4,096 bytes
One-off event length366 days
Length of each date of a series31 days
Years1900 to 9999
List limit1 to 100, default 50
List window (after to before)366 days. after defaults to now and before to 90 days after after

For recurrence limits, see Recurring Events. Calendar requests count toward your API rate limits like any other request.

Send limits for invitations

Invitation, update, cancellation and reply emails also count toward your organization, pod and inbox send limits, one send per recipient. The charge is taken when you make the request, before anything is saved, so an over-limit request returns 429 rate_limit_exceeded and changes nothing. See Send limits.

Retention

The record of each date, including its start and end webhook outcome, is kept for 30 days after the date ends (for a date cancelled with DELETE, 30 days after the cancel). Reading or changing an older date returns 410 event_expired. One-off and series events themselves are kept until you delete them.

Errors

Calendar errors use the standard error format, with a stable code to branch on and a fix that says what to do next.

StatuscodeMeaning
400validation_errorA field is missing or invalid. errors lists each problem with its path.
400query_range_too_wideThe after to before window is wider than 366 days, or contains too many dates of a series. Request a smaller window.
403forbiddenThe organization does not have calendar access (private beta).
403missing_permissionThe API key lacks the calendar permission the endpoint needs. See Permissions.
404not_foundThe inbox, event or date does not exist or is not visible to this API key. Also returned for a deleted event or a date cancelled with DELETE.
409idempotency_conflictclient_id was already used for a different create request, or the Idempotency-Key was already used to delete a different event. Delete keys are unique across your organization.
409race_conditionA concurrent change conflicted with this one (for a change sent without If-Match, the resource changed while the request ran). Retry; for creates, retry with the same client_id. Also returned at the calendar’s event limit (10,000 by default) or a series’ edited-date limits (730 individual dates or 64 future edits), where retrying cannot help. Check event_count with Get Calendar and the series’ existing edits.
409event_startingThe event or date is starting right now, so its schedule cannot change for a few seconds. Retry shortly.
409event_already_startedThe event or date has started (its start is fixed) or ended (only descriptive fields can change).
409event_in_progressA series with edited dates cannot become a one-off event.
409future_edit_start_move_not_supportedA mode=future change cannot move start. Use mode=single, or update the series.
409recurrence_density_limitThe rule produces more than 730 dates in some 90-day window.
409materialization_failedThe rule’s first date could not be found within the evaluation bound. Add COUNT or UNTIL, or simplify the rule.
409page_token_staleThe series changed while you were paging through its dates. Restart the list without page_token.
409calendar_response_invalidSending invitations for an event the inbox does not organize, or responding to an event that did not come from an invitation to this inbox.
410event_expiredThe date ended more than 30 days ago, or was removed from its series.
412precondition_failedYou sent If-Match, and the resource changed since that etag was read. The body includes current_revision.
413payload_too_largeThe request body exceeds 128 KB (events) or 8 KB (calendar settings).
429rate_limit_exceededToo many requests, or the invitation emails this request would send exceed a send limit. Nothing was changed. Wait for Retry-After when present, or retry without send_invites.