---
title: "client.calendar"
description: "Every method in this namespace: its signature, its parameters, what it returns and an example."
url: "https://openemail.uk/docs/ruby/reference/calendar"
area: "Ruby"
category: "Reference"
---

# client.calendar

Every method in this namespace: its signature, its parameters, what it returns and an example.

## Methods

Calendar events found in mail, expanded into occurrences.

### `calendar.list_events`

List one page of event occurrences inside a time window

```ruby
list_events(from:, to:, timezone: nil, limit: nil, cursor: nil, api_key: nil) -> OpenEmail::Page
```

Expands every event in the workspace into its occurrences between `from` and `to` and returns one page of them, sorted by start time and then by event id. A recurring series gives one row per instance in the window, with its excluded dates left out, and every row points back to its event through `eventId`. The window is required because a repeating series has no end to list up to. Follow `next_cursor` while `has_more?` is true, with the same `from:`, `to:` and `timezone:`, to read every occurrence, or let `list_all_events` and `iterate_events` do that walk.

`to` must be after `from` and at most 366 days later. A missing `from` or `to` is a 422 `invalid_parameter`, while a value that does not parse, a reversed or overlong window, or an unknown zone is a 400 `invalid_parameter` naming the field. Both bounds take a Time, DateTime, Date or ISO 8601 String, and the SDK sends a Time or DateTime as a UTC ISO 8601 instant.

`timezone` is an IANA zone and defaults to `UTC` on the server. It is used to expand any event stored without a zone of its own, which is what decides the day an all day event lands on.

Scopes: `calendar:read`.

**Parameters**

- `from` (`Time, DateTime, Date or String`, required): Start of the window.
- `to` (`Time, DateTime, Date or String`, required): End of the window, after `from` and at most 366 days later.
- `timezone` (`String`): IANA zone such as `Europe/London`, at most 64 characters. Defaults to `UTC`.
- `limit` (`Integer`): Occurrences per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`String`): The `next_cursor` of the previous page, sent with the same window. It is opaque, so never build one yourself.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

An `OpenEmail::Page` of occurrence Hashes, with `items`, `has_more?` and `next_cursor`. Each item has `eventId`, `uid`, `start`, `end`, `allDay`, `summary`, `location`, `status`, `transparency`, `isOrganizer`, `organizerEmail`, `recurring`, `myPartstat` and `attendeeCount`.

**Example**

```ruby
from = Time.now
to = from + 7 * 24 * 60 * 60

page = client.calendar.list_events(from:, to:, timezone: "Europe/London", limit: 50)

page.items.each do |occurrence|
  puts "#{occurrence[:start]} #{occurrence[:summary]} #{occurrence[:myPartstat] || "not invited"}"
end

p page.has_more?, page.next_cursor
```

**Notes**

- `myPartstat` is the response recorded for any address this workspace holds on its own domains, or for the addresses a narrowed key covers, and nil when none of them is an attendee. A workspace with no address on its own domains matches no attendee, so every row reads nil.
- A narrowed key only sees occurrences whose organiser is one of the addresses it covers or where one of them is an attendee.
- Cancelled events are not filtered out. Check `status` for `CANCELLED`.
- Nothing in the window is dropped. A series repeats at most once a day, so one series gives at most one row per day of the window, and every one of them is reachable through the pages.
- The cursor holds the start time and event id of the last row, so an event deleted or moved between pages never breaks the walk. A cursor this list did not hand out is a 400 `invalid_cursor`.

Also available in: API [`GET /calendar/events`](https://openemail.uk/docs/api/reference/calendar#get-calendar-events); TypeScript [`calendar.listEvents()`](https://openemail.uk/docs/sdk/reference/calendar#listEvents); Python [`calendar.list_events()`](https://openemail.uk/docs/python/reference/calendar#listEvents); CLI [`openemail calendar list-events`](https://openemail.uk/docs/cli/reference/calendar#calendar-list-events).

### `calendar.list_all_events`

Collect every occurrence inside a time window into one array

```ruby
list_all_events(from:, to:, timezone: nil, limit: nil, cursor: nil, api_key: nil) -> Array<Hash>
```

Walks every page of `list_events` for one window and returns all of its occurrences, sorted by start time and then by event id. It follows `next_cursor` until `has_more?` is false, sending the same `from:`, `to:` and `timezone:` with each request.

A window of up to 366 days of a busy calendar can hold thousands of occurrences, so prefer `iterate_events` when you can stop early.

Scopes: `calendar:read`.

**Parameters**

- `from` (`Time, DateTime, Date or String`, required): Start of the window.
- `to` (`Time, DateTime, Date or String`, required): End of the window, after `from` and at most 366 days later.
- `timezone` (`String`): IANA zone such as `Europe/London`, at most 64 characters. Defaults to `UTC`.
- `limit` (`Integer`): Page size for each request, 1 to 100. The server defaults to 25.
- `cursor` (`String`): Starts the walk from this cursor instead of the start of the window.
- `api_key` (`String`): Overrides the client API key for every page of this walk.

**Returns**

An Array of occurrence Hashes holding every occurrence in the window, earliest first.

**Example**

```ruby
occurrences = client.calendar.list_all_events(
  from: "2026-10-01T00:00:00Z",
  to: "2026-11-01T00:00:00Z",
  limit: 100
)

busy = occurrences.select { |occurrence| occurrence[:transparency] == "OPAQUE" }

puts "#{busy.length} busy blocks in October"
```

**Notes**

- If any page fails the call raises, and the occurrences already fetched are discarded.

Also available in: API [`GET /calendar/events`](https://openemail.uk/docs/api/reference/calendar#get-calendar-events); TypeScript [`calendar.listAllEvents()`](https://openemail.uk/docs/sdk/reference/calendar#listAllEvents); Python [`calendar.list_all_events()`](https://openemail.uk/docs/python/reference/calendar#listAllEvents).

### `calendar.iterate_events`

Stream the occurrences inside a time window one at a time

```ruby
iterate_events(from:, to:, timezone: nil, limit: nil, cursor: nil, api_key: nil, &block) -> Enumerator<Hash>
```

Returns an Enumerator over the occurrences in one window that yields them individually, earliest first, or yields each one to a block when given one, and fetches the next page only when the current one is drained. Without a block nothing is requested until you consume it, and breaking out of the loop stops further requests, which makes it the way to find the next free slot or the next meeting without reading the whole window.

The walk ends when `has_more?` is false, when a page carries no `next_cursor`, or when the server repeats a cursor.

Scopes: `calendar:read`.

**Parameters**

- `from` (`Time, DateTime, Date or String`, required): Start of the window.
- `to` (`Time, DateTime, Date or String`, required): End of the window, after `from` and at most 366 days later.
- `timezone` (`String`): IANA zone such as `Europe/London`, at most 64 characters. Defaults to `UTC`.
- `limit` (`Integer`): Page size per request, 1 to 100. The server defaults to 25.
- `cursor` (`String`): Starts the walk from this cursor instead of the start of the window.
- `api_key` (`String`): Overrides the client API key for every page of this walk.

**Returns**

An Enumerator of occurrence Hashes (or yields each one to a block), one occurrence per step.

**Example**

```ruby
from = Time.now
to = from + 30 * 24 * 60 * 60

meeting = client.calendar.iterate_events(from:, to:).find { |occurrence| occurrence[:myPartstat] }

puts "next meeting #{meeting[:start]} #{meeting[:summary]}" if meeting
```

**Notes**

- A page request that fails raises out of the loop, after every occurrence of the pages before it has been yielded.

Also available in: API [`GET /calendar/events`](https://openemail.uk/docs/api/reference/calendar#get-calendar-events); TypeScript [`calendar.iterateEvents()`](https://openemail.uk/docs/sdk/reference/calendar#iterateEvents); Python [`calendar.iterate_events()`](https://openemail.uk/docs/python/reference/calendar#iterateEvents).

### `calendar.get_event`

Read an event with its attendees

```ruby
get_event(id, api_key: nil) -> Hash
```

Returns a stored event with its attendees and their responses. What the server understood from the iCalendar data is structured here: `recurrence` holds the `RRULE` text, `exdates` the excluded instances, `organizer` the organiser and `alarms` the reminders, each a Hash with `minutesBefore` and `action`. The raw iCalendar source is not included, and `get_event_ics` serves the event as a document.

`id` is the event id, the `eventId` on an occurrence, and not the iCalendar `uid`. A recurring series is one event however many occurrences it has, so `start` and `end` describe its first instance. `messageId` and `threadId` link the event to mail in the mailbox when there is any.

Attendees come back in the order they were added, each with `partstat`, `role`, `rsvp`, `cutype` and `respondedAt`. A narrowed key gets a 404 unless one of the addresses it covers organises the event or is on the attendee list, and a key that holds a whole domain covers every address on it.

Scopes: `calendar:read`.

**Parameters**

- `id` (`String`, required): Event id, the `eventId` of an occurrence, such as `cal_` followed by 24 hex characters.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash with `id`, `uid`, `sequence`, `summary`, `description`, `location`, `url`, `start`, `end`, `allDay`, `timezone`, `recurrence`, `exdates`, `status`, `transparency`, `visibility`, `organizer`, `isOrganizer`, `source`, `messageId`, `threadId`, `alarms`, `attendees`, `createdAt` and `updatedAt`.

**Example**

```ruby
event = client.calendar.get_event("cal_3f9a1c7e5b2d48a06c1e9f4b")

puts event[:summary], event[:recurrence] || "one off"
p event[:attendees].map { |attendee| "#{attendee[:email]}: #{attendee[:partstat]}" }
```

**Notes**

- `status`, `transparency` and `visibility` carry iCalendar values, and default to `CONFIRMED`, `OPAQUE` and `PUBLIC`.
- An attendee's `partstat` defaults to `NEEDS-ACTION` and `role` to `REQ-PARTICIPANT` until a response is recorded.

Also available in: API [`GET /calendar/events/{id}`](https://openemail.uk/docs/api/reference/calendar#get-calendar-events-id); TypeScript [`calendar.getEvent()`](https://openemail.uk/docs/sdk/reference/calendar#getEvent); Python [`calendar.get_event()`](https://openemail.uk/docs/python/reference/calendar#getEvent); CLI [`openemail calendar get-event`](https://openemail.uk/docs/cli/reference/calendar#calendar-get-event).

### `calendar.get_event_ics`

Download an event as an iCalendar document

```ruby
get_event_ics(id, api_key: nil) -> String
```

Returns the event serialised as an `.ics` document, as a raw String rather than a parsed Hash. The server sends it as `text/calendar; charset=utf-8` with a download filename built from the event's `uid`, and the SDK requests that type and returns the body as a UTF-8 String without parsing it.

The document declares `METHOD:PUBLISH`, and the content type carries no `method` parameter. That is deliberate: a `REQUEST` document makes a calendar client offer accept and decline and reply to the organiser, and this endpoint has no authority to invite anyone. Import it to show what the event currently is, not to send an invitation.

It holds one `VEVENT` with the attendees and their participation status, the recurrence rule, excluded dates and alarms. Lines end in CRLF and long lines are folded. `DTSTAMP` is the time of the request, so two downloads of an unchanged event differ on that line.

Scopes: `calendar:read`.

**Parameters**

- `id` (`String`, required): Event id, the `eventId` of an occurrence.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A String, the iCalendar text from `BEGIN:VCALENDAR` to `END:VCALENDAR`.

**Example**

```ruby
ics = client.calendar.get_event_ics("cal_8d3f0a2b9c4e41f7a6b5c2d1")
summary = ics.split("\r\n").find { |line| line.start_with?("SUMMARY:") }

puts summary
```

**Notes**

- Errors still come back as JSON and raise `OpenEmail::ApiError`, so a failure never comes back as a String.
- Timed events are written as UTC instants and all day events as `DATE` values, never as local times with a `TZID`.
- A narrowed key gets a 404 on the same terms as `get_event`.

Also available in: API [`GET /calendar/events/{id}/ics`](https://openemail.uk/docs/api/reference/calendar#get-calendar-events-id-ics); TypeScript [`calendar.getEventIcs()`](https://openemail.uk/docs/sdk/reference/calendar#getEventIcs); Python [`calendar.get_event_ics()`](https://openemail.uk/docs/python/reference/calendar#getEventIcs); CLI [`openemail calendar get-event-ics`](https://openemail.uk/docs/cli/reference/calendar#calendar-get-event-ics).

### `calendar.create_event`

Put an event on the calendar

```ruby
create_event(body = nil, api_key: nil, **fields) -> Hash
```

Creates an event, as New event on the Calendar page of the app does. `start` and `end` take a `Time` or ISO 8601 with a zone, and the SDK sends a `Time` as a UTC instant. `timezone` is the IANA zone the event belongs to, which decides what an all-day event means.

With `attendees`, an invitation goes to each of them by email from `from`, which has to be an address the key may send as, so the call also needs `emails:send`. Send `sendInvites: false` to keep it to your own calendar.

Scopes: `calendar:write`.

**Parameters**

- `summary` (`String`, required): The title, 1 to 255 characters.
- `start` (`Time, DateTime, Date or String`, required): When it starts, a `Time` or ISO 8601 with a zone.
- `end` (`Time, DateTime, Date or String`, required): When it ends, after `start` and at most two years later.
- `description` (`String`): Notes about the event, up to 8,000 characters.
- `location` (`String`): Where it is: a room, an address or a link.
- `url` (`String`): A link that belongs to the event.
- `allDay` (`Boolean`): Whether it fills whole days. `start` and `end` then mark the days.
- `timezone` (`String`): The IANA zone the event belongs to, `UTC` unless you say.
- `recurrence` (`String`): An RRULE such as `FREQ=WEEKLY;BYDAY=MO` when it repeats.
- `attendees` (`Array<Hash>`): Up to 200 people to invite, each a Hash with `email` and, if you like, `name` and `optional`.
- `reminders` (`Array<Hash>`): Up to five reminders, each a Hash with `minutesBefore` and, if you like, `action`.
- `transparency` (`String`): `OPAQUE` shows you busy and `TRANSPARENT` free.
- `visibility` (`String`): `PUBLIC`, `PRIVATE` or `CONFIDENTIAL`.
- `sendInvites` (`Boolean`): False keeps the change to your own calendar, and sends nothing.
- `from` (`String`): The address the invitations come from. It has to be one the key may send as.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash for the new event, shaped like the one `get_event` returns, with its attendees and their responses.

**Example**

```ruby
event = client.calendar.create_event(
  summary: "Planning",
  start: Time.utc(2026, 10, 12, 9, 0),
  end: Time.utc(2026, 10, 12, 9, 30),
  attendees: [{email: "ada@example.com"}],
  from: "team@acme.com"
)

puts event[:id], event[:attendees].length
```

**Notes**

- The SDK does not retry it, because a second call makes a second event and invites everybody again.
- A key that may not send as `from` is refused with 403 `from_address_forbidden`, and an event with attendees and no `from` with 422 `invalid_parameter`. Invitations without `emails:send` are a 403 `insufficient_scope`.

Also available in: API [`POST /calendar/events`](https://openemail.uk/docs/api/reference/calendar#post-calendar-events); TypeScript [`calendar.createEvent()`](https://openemail.uk/docs/sdk/reference/calendar#createEvent); Python [`calendar.create_event()`](https://openemail.uk/docs/python/reference/calendar#createEvent); CLI [`openemail calendar create-event`](https://openemail.uk/docs/cli/reference/calendar#calendar-create-event).

### `calendar.update_event`

Change an event

```ruby
update_event(id, patch = nil, api_key: nil, **fields) -> Hash
```

Changes an event you organise. A field you leave out keeps its value, and `attendees` replaces the whole list. Everyone invited gets the updated invitation by email from `from`, so a change to an event with attendees needs `emails:send` too, unless `sendInvites` is false.

Scopes: `calendar:write`.

**Parameters**

- `id` (`String`, required): Event id, the `eventId` of an occurrence, such as `cal_` followed by 24 hex characters.
- `summary` (`String`): The title, 1 to 255 characters.
- `start` (`Time, DateTime, Date or String`): When it starts, a `Time` or ISO 8601 with a zone.
- `end` (`Time, DateTime, Date or String`): When it ends, after `start` and at most two years later.
- `description` (`String`): Notes about the event, up to 8,000 characters.
- `location` (`String`): Where it is: a room, an address or a link.
- `url` (`String`): A link that belongs to the event.
- `allDay` (`Boolean`): Whether it fills whole days. `start` and `end` then mark the days.
- `timezone` (`String`): The IANA zone the event belongs to, `UTC` unless you say.
- `recurrence` (`String`): An RRULE such as `FREQ=WEEKLY;BYDAY=MO` when it repeats.
- `attendees` (`Array<Hash>`): Up to 200 people to invite, each a Hash with `email` and, if you like, `name` and `optional`. It replaces the whole list.
- `reminders` (`Array<Hash>`): Up to five reminders, each a Hash with `minutesBefore` and, if you like, `action`.
- `transparency` (`String`): `OPAQUE` shows you busy and `TRANSPARENT` free.
- `visibility` (`String`): `PUBLIC`, `PRIVATE` or `CONFIDENTIAL`.
- `sendInvites` (`Boolean`): False keeps the change to your own calendar, and sends nothing.
- `from` (`String`): The address the invitations come from. It has to be one the key may send as.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

The event Hash as it stands after the change, shaped like the one `get_event` returns, with `sequence` one higher.

**Example**

```ruby
event = client.calendar.update_event(
  "cal_3f9a1c7e5b2d48a06c1e9f4b",
  start: "2026-10-12T10:00:00Z",
  end: "2026-10-12T10:30:00Z"
)

puts event[:start], event[:sequence]
```

**Notes**

- The SDK does not retry it, because a retry after a lost response sends the update to everybody again.
- An event organised by somebody else is refused with 403 `not_organizer`: answer it with `respond_to_event` instead. An event the key does not reach is a 404.

Also available in: API [`PATCH /calendar/events/{id}`](https://openemail.uk/docs/api/reference/calendar#patch-calendar-events-id); TypeScript [`calendar.updateEvent()`](https://openemail.uk/docs/sdk/reference/calendar#updateEvent); Python [`calendar.update_event()`](https://openemail.uk/docs/python/reference/calendar#updateEvent); CLI [`openemail calendar update-event`](https://openemail.uk/docs/cli/reference/calendar#calendar-update-event).

### `calendar.delete_event`

Remove an event from the calendar

```ruby
delete_event(id, api_key: nil) -> Hash
```

Removes an event for good, as Remove does in the app. Nobody else is told, so call off a meeting with others in it with `cancel_event` instead.

Scopes: `calendar:write`.

**Parameters**

- `id` (`String`, required): Event id, the `eventId` of an occurrence, such as `cal_` followed by 24 hex characters.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash with `object`, `id` and `deleted` set to true.

**Example**

```ruby
client.calendar.delete_event("cal_3f9a1c7e5b2d48a06c1e9f4b")
```

**Notes**

- An event organised by somebody else is refused with 403 `not_organizer`, and an event that is gone or that the key does not reach is a 404.

Also available in: API [`DELETE /calendar/events/{id}`](https://openemail.uk/docs/api/reference/calendar#delete-calendar-events-id); TypeScript [`calendar.deleteEvent()`](https://openemail.uk/docs/sdk/reference/calendar#deleteEvent); Python [`calendar.delete_event()`](https://openemail.uk/docs/python/reference/calendar#deleteEvent); CLI [`openemail calendar delete-event`](https://openemail.uk/docs/cli/reference/calendar#calendar-delete-event).

### `calendar.cancel_event`

Call off a meeting

```ruby
cancel_event(id, body = nil, api_key: nil, **fields) -> Hash
```

Cancels a meeting you organise, as Cancel meeting does in the app. It stays on the calendar with `status` `CANCELLED`, and everyone invited is told it is off by email from `from`, so a meeting with attendees also needs `emails:send`.

Scopes: `calendar:write`.

**Parameters**

- `id` (`String`, required): Event id, the `eventId` of an occurrence, such as `cal_` followed by 24 hex characters.
- `from` (`String`): The address the cancellation comes from. It has to be one the key may send as.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

The event Hash, shaped like the one `get_event` returns, with `status` set to `CANCELLED`.

**Example**

```ruby
event = client.calendar.cancel_event("cal_3f9a1c7e5b2d48a06c1e9f4b", from: "team@acme.com")

puts event[:status]
```

**Notes**

- The SDK does not retry it, because a retry sends the cancellation again.
- An event organised by somebody else is refused with 403 `not_organizer`.

Also available in: API [`POST /calendar/events/{id}/cancel`](https://openemail.uk/docs/api/reference/calendar#post-calendar-events-id-cancel); TypeScript [`calendar.cancelEvent()`](https://openemail.uk/docs/sdk/reference/calendar#cancelEvent); Python [`calendar.cancel_event()`](https://openemail.uk/docs/python/reference/calendar#cancelEvent); CLI [`openemail calendar cancel-event`](https://openemail.uk/docs/cli/reference/calendar#calendar-cancel-event).

### `calendar.respond_to_event`

Answer an invitation

```ruby
respond_to_event(id, body = nil, api_key: nil, **fields) -> Hash
```

Accepts, declines or tentatively accepts an invitation, as the buttons of an invitation do in the app. The answer goes to the organiser by email from the invited address the key may send as, or from `respondingAs` when the invitation went to more than one of yours, so it needs `emails:send` as well.

Scopes: `calendar:write`, `emails:send`.

**Parameters**

- `id` (`String`, required): Event id, the `eventId` of an occurrence, such as `cal_` followed by 24 hex characters.
- `response` (`String`, required): `ACCEPTED`, `DECLINED` or `TENTATIVE`.
- `respondingAs` (`String`): Which of your invited addresses answers, when there is more than one.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

The event Hash, shaped like the one `get_event` returns, with your answer on your attendee row.

**Example**

```ruby
event = client.calendar.respond_to_event("cal_3f9a1c7e5b2d48a06c1e9f4b", response: "ACCEPTED")

p event[:attendees].map { |attendee| "#{attendee[:email]}: #{attendee[:partstat]}" }
```

**Notes**

- An event that invited none of the addresses the key may send as is refused with 403 `not_invited`, and a cancelled meeting with 409 `event_cancelled`.
- When the organiser cannot be reached, nothing changes and the call is a 409 `reply_not_sent`.

Also available in: API [`POST /calendar/events/{id}/respond`](https://openemail.uk/docs/api/reference/calendar#post-calendar-events-id-respond); TypeScript [`calendar.respondToEvent()`](https://openemail.uk/docs/sdk/reference/calendar#respondToEvent); Python [`calendar.respond_to_event()`](https://openemail.uk/docs/python/reference/calendar#respondToEvent); CLI [`openemail calendar respond-to-event`](https://openemail.uk/docs/cli/reference/calendar#calendar-respond-to-event).
