Events, recurrence and time
How Calendarpit expands recurring events, simulates invitations and meetings locally, and handles time zones, DST, and free/busy intervals.
Calendarpit handles these locally:
- expanding recurring series
- syncing invitations between local test users
- calculating time ranges and free/busy intervals
For supported endpoints and fields, see Calendar API compatibility.
Recurrence
You can read a recurring event as a series or as expanded occurrences.
In events.list, singleEvents controls the shape:
singleEvents=false(default): returns series masters and stored exceptions.singleEvents=true: expands occurrences.
The instances endpoint
GET /calendar/v3/calendars/{calendarId}/events/{eventId}/instances always returns expanded occurrences for one series.
- With
singleEvents=false, cancelled exceptions appear withoutshowDeleted. - It accepts the
events.listparameters, plusoriginalStart. Examples includetimeMin,timeMax,showDeleted,maxResults,pageToken, andtimeZone. - It rejects
syncToken. - It omits
nextSyncToken.
Instance IDs and exceptions
Each instance carries recurringEventId, originalStartTime, and a deterministic ID:
- Timed events:
<seriesId>_<YYYYMMDDTHHMMSSZ>, in UTC. - All-day events:
<seriesId>_<YYYYMMDD>.
On /events/{instanceId}, each method works on a single occurrence:
| Method | Behavior |
|---|---|
GET | Returns the occurrence, even if no exception is stored. |
PUT, PATCH | Stores the occurrence as an exception. |
DELETE | Stores a cancelled exception. |
Changes to the master affect its occurrences:
- Updating a master updates its instances. It also cancels stored exceptions that the new rule no longer produces.
- Deleting a master cancels its exceptions.
Expansion also applies to other features:
- With
singleEvents=true, incremental sync reports removed occurrences ascancelled. freebusy.queryuses the same expansion.
Rules and validation
recurrence accepts 1–20 RRULE, EXRULE, RDATE, or EXDATE lines. An empty array stores a non-recurring event.
These return 400 unsupportedFeature:
- other lines, such as
DTSTART - a missing
FREQ - a sub-daily
FREQ(HOURLY,MINUTELY,SECONDLY) - uppercase rule parts not listed below, such as
BYHOUR - duplicate rule parts
These return 400 invalidParameter:
- malformed parts
- invalid values, combinations, or line counts
Supported rule parts:
| Rule part | Supported values |
|---|---|
FREQ | DAILY, WEEKLY, MONTHLY, YEARLY |
INTERVAL | 1–10000 |
COUNT | 1–10000. Use COUNT or UNTIL, not both. |
UNTIL | Timed UNTIL must be UTC (...Z). All-day UNTIL is a date. |
BYDAY | MO–SU. On MONTHLY and YEARLY, an optional ±1–5 is allowed. |
BYMONTHDAY | ±1–31 |
BYMONTH | 1–12 |
BYSETPOS | ±1–366 |
WKST | Supported |
Time zone requirements:
- Timed series: require
start.timeZone. RDATEandEXDATE: Timed series allow;TZID=<zone>. All-day series require;VALUE=DATE.
Expansion limits
Expansion has two limits. Exceeding either returns 400 unsupportedFeature.
- Instance count: At most 10,000 instances per expansion. The count applies per request window, or to the whole series when there is no
timeMax. - Time span: At most 100 years (366×100 days) from
DTSTART. This applies to finite series expanded withouttimeMax, and totimeMinandtimeMax.
Unbounded series (no COUNT or UNTIL) require timeMax in these cases:
- when expanding (
singleEvents=true) - when
timeMinis passed toevents.list
One series over a limit fails the whole list or freeBusy request. Deleted series still count. Send timeMax or reset state.
Daylight saving time (DST)
Rules keep wall-clock time in start.timeZone.
Nonexistent local times (the spring-forward gap) fail with 400 unsupportedFeature when an expansion reaches the gap occurrence. This affects:
instancesevents.listwithsingleEvents=trueor withtimeMin/timeMax- instance IDs for occurrences within about two days of the gap
freeBusy
The insert itself succeeds. The error fails the whole request.
Ambiguous times (the fall-back fold) use the earlier, pre-transition offset, such as -04:00 in New York.
Invitations, reminders, and local meetings
Calendarpit never sends email or calls Google Meet.
Invitations
attendees accepts up to 200 unique emails. Each attendee supports displayName, optional, resource, additionalGuests, comment, and responseStatus. responseStatus is one of needsAction, accepted, tentative, or declined.
Inviting a local test user with a primary calendar writes a copy to that calendar. The copy has the same id and iCalUID.
- Organizer updates and cancellations propagate to copies and trigger watch notifications.
- Removing an attendee cancels their copy.
remindersandextendedProperties.privatestay per-copy.extendedProperties.sharedcomes from the organizer.
RSVP and invitee edits
Invitees must use PATCH. PUT returns 403 forbiddenForNonOrganizer.
Invitees may only edit:
- their own
responseStatusandcomment, or setattendeesOmitted: truewith only their own entry reminderscolorIdtransparencyextendedProperties
Effects of invitee edits:
- RSVP changes update the organizer and other copies.
- An RSVP to one instance of a series creates an exception in the organizer's calendar.
- Declining sets the invitee's copy to
transparent. Otherattendeespatches reset it toopaque. - The next organizer update overwrites
colorIdandtransparencychanges on a copy.
Guest flags
| Flag | Default |
|---|---|
guestsCanModify | false |
guestsCanInviteOthers | true |
guestsCanSeeOtherGuests | true |
- These flags round-trip as booleans.
- Only the organizer can change them.
- They are stored only. They do not change what invitees may edit.
sendUpdates
| Value | What Calendarpit records |
|---|---|
all | One entry per write. recipients holds all attendees except the organizer, including removed attendees. For an invitee's REPLY, the recipient is the organizer. |
externalOnly | Only recipients who are not local users. |
none (default) | Nothing. |
- Entries (
REQUEST,CANCEL, orREPLY) appear inGET /__calendarpit/stateundernotifications. It keeps the last 200. See Test control API. - Local copies sync regardless of
sendUpdates.
Reminders
- Shape:
{ useDefault: boolean, overrides?: [{ method: "email" | "popup", minutes: 0..40320 }] }. - Limit: At most 5 overrides when
useDefault: false. - Behavior: Reminders are stored only. They never fire.
Conference data
- Non-null
conferenceDatarequires?conferenceDataVersion=1. - A
hangoutsMeetcreateRequest.requestIdgenerates a deterministic ID, ahangoutLink, and avideoentry point athttps://meet.calendarpit.invalid/<id>. conferenceData: nullclears it.
Time semantics and free/busy
Event intervals are half-open [start, end). end must be after start.
List filters match start < timeMax and end > timeMin. As a result, adjacent events do not overlap.
All-day events
start.dateand exclusiveend.dateuseYYYY-MM-DD.- They use midnight in the calendar's time zone.
- This also holds on 23-hour and 25-hour DST days.
Timed events
dateTimerequires a UTC offset orZ, or an IANAtimeZone.- Unoffset local times in a DST gap or overlap return
400. - Responses format
dateTimewith a numeric offset in the requestedtimeZone, or in the calendar's zone. - The response's
timeZoneis also the requested or calendar zone. It is not the event's stored zone.
Free/busy
POST /calendar/v3/freeBusy queries up to 50 calendars. It returns busy ranges formatted in timeZone (default UTC).
When it computes busy ranges, it:
- expands recurring series
- excludes
transparentevents (free, including declined invitations) - excludes
cancelled(deleted) events - merges overlapping or adjacent intervals
- clips them to
[timeMin, timeMax)
Unknown or unowned calendars return a per-calendar notFound error with busy: [].