# How to list iCloud calendar events

Source: https://developer.nylas.com/docs/cookbook/calendar/events/list-events-icloud/

Apple has no public calendar REST API. iCloud Calendar runs on CalDAV, an XML-based protocol that requires persistent connections and manual credential management. There's no developer portal, no SDK, and no way to programmatically generate the app-specific passwords that Apple requires for third-party access. Nylas handles the CalDAV connection for you and exposes iCloud Calendar through the same [Events API](/docs/reference/api/events/) you use for Google and Microsoft.

This guide covers listing events from iCloud Calendar accounts, including the app-specific password requirement, the one-year time range cap, and iCloud-specific behaviors that affect how you query events.

## How do I list iCloud calendar events with the Nylas API?

Send a `GET` request to `/v3/grants/{grant_id}/events` using the iCloud connector, your API key, and a `calendar_id`. The endpoint returns up to 50 events per page by default, sorted by start time. iCloud doesn't accept `calendar_id=primary`, so fetch the calendar ID first with List Calendars. See [List events](#list-events) for the request and response.

## Why use Nylas instead of CalDAV directly?

CalDAV works, but building on it directly is a significant investment:

- **XML everywhere.** CalDAV uses WebDAV extensions with XML request and response bodies. Nylas gives you REST endpoints with JSON.
- **Persistent connections required.** You need to maintain long-lived CalDAV sessions per user. Nylas manages connection pooling and reconnection behind the scenes.
- **No programmatic password generation.** Every user must manually create an app-specific password through their Apple ID settings. Nylas guides users through this during authentication, but the manual step can't be eliminated.
- **No real-time push.** CalDAV has no native push notification system comparable to Google's or Microsoft's. Nylas provides [webhooks](/docs/v3/notifications/) for change notifications across all providers.
- **Limited query capabilities.** CalDAV supports basic time-range queries but lacks the rich filtering that Google Calendar or Microsoft Graph offer. Nylas normalizes what it can and clearly documents the gaps.

If you're comfortable with XML parsing and only need iCloud, CalDAV works fine. For multi-provider apps or faster development, Nylas saves you from building and maintaining a CalDAV client.

## Before you begin

You'll need:

- A [Nylas application](/docs/v3/getting-started/) with a valid API key
- A [grant](/docs/v3/auth/) for an iCloud account using the **iCloud connector** (not generic IMAP)
- An iCloud connector configured in your Nylas application


> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) to set up your app and connect a test account before continuing here.


### App-specific passwords

iCloud requires **app-specific passwords** for third-party access. Unlike Google or Microsoft OAuth, there's no way to generate these programmatically. Each user must create one manually in their Apple ID settings.

Nylas supports two authentication flows for iCloud:

| Method                              | Best for                                                               |
| ----------------------------------- | ---------------------------------------------------------------------- |
| Hosted OAuth                        | Production apps where Nylas guides users through the app password flow |
| Bring Your Own (BYO) Authentication | Custom auth pages where you collect credentials directly               |

With either method, users need to:

1. Go to [appleid.apple.com](https://appleid.apple.com/) and sign in
2. Navigate to **Sign-In and Security** then **App-Specific Passwords**
3. Generate a new app password
4. Use that password (not their regular iCloud password) when authenticating

> **Warn:** 
> **App-specific passwords can't be generated via API.** Your app's onboarding flow should include clear instructions telling users how to create one. Users who enter their regular iCloud password will fail authentication.

The full setup walkthrough is in the [iCloud provider guide](/docs/provider-guides/icloud/) and the [app passwords guide](/docs/provider-guides/app-passwords/).

## List events

Make a [List Events request](/docs/reference/api/events/get-all-events/) with the grant ID and a `calendar_id`. By default, Nylas returns up to 50 events sorted by start time.

> **Warn:** 
> **iCloud does not support `calendar_id=primary`.** You must call the [List Calendars endpoint](/docs/reference/api/calendar/get-all-calendars/) first to get the actual calendar ID for the account.

```bash
curl --compressed --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>&start=<TIMESTAMP>&end=<TIMESTAMP>' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json'

```

```json
{
  "request_id": "cbd60372-df33-41d3-b203-169ad5e3AAAA",
  "data": [
    {
      "busy": true,
      "calendar_id": "primary",
      "conferencing": {
        "details": {
          "meeting_code": "ist-****-tcz",
          "url": "https://meet.google.com/ist-****-tcz"
        },
        "provider": "Google Meet"
      },
      "created_at": 1701974804,
      "creator": {
        "email": "anna.molly@example.com",
        "name": ""
      },
      "description": null,
      "grant_id": "1e3288f6-124e-405d-a13a-635a2ee54eb2",
      "hide_participants": false,
      "html_link": "https://www.google.com/calendar/event?eid=NmE0dXIwabQAAAA",
      "ical_uid": "6aaaaaaame8kpgcid6hvd0q@google.com",
      "id": "6aaaaaaame8kpgcid6hvd",
      "object": "event",
      "organizer": {
        "email": "anna.molly@example.com",
        "name": ""
      },
      "participants": [
        {
          "email": "jenna.doe@example.com",
          "status": "yes"
        },
        {
          "email": "anna.molly@example.com",
          "status": "yes"
        }
      ],
      "read_only": true,
      "reminders": {
        "overrides": null,
        "use_default": true
      },
      "status": "confirmed",
      "title": "Holiday check in",
      "updated_at": 1701974915,
      "when": {
        "end_time": 1701978300,
        "end_timezone": "America/Los_Angeles",
        "object": "timespan",
        "start_time": 1701977400,
        "start_timezone": "America/Los_Angeles"
      }
    }
  ]
}


```

```js [listEvents-Node.js SDK]

import Nylas from "nylas";

const nylas = new Nylas({
  apiKey: "<NYLAS_API_KEY>",
  apiUri: "<NYLAS_API_URI>",
});

async function fetchAllEventsFromCalendar() {
  try {
    const events = await nylas.events.list({
      identifier: "<NYLAS_GRANT_ID>",
      queryParams: {
        calendarId: "<CALENDAR_ID>",
      },
    });

    console.log("Events:", events);
  } catch (error) {
    console.error("Error fetching calendars:", error);
  }
}

fetchAllEventsFromCalendar();


```

```python [listEvents-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"

events = nylas.events.list(
    grant_id,
    query_params={
      "calendar_id": "<CALENDAR_ID>"
    }
)

print(events)

```


```ruby [listEvents-Ruby SDK]

require 'nylas'

nylas = Nylas::Client.new(api_key: "<NYLAS_API_KEY>")

query_params = {
	calendar_id: "<CALENDAR_ID>"
}

# Read events from our main calendar in the specified date and time
events, _request_ids = nylas.events.list(identifier: "<NYLAS_GRANT_ID>", query_params: query_params)

events.each {|event|
	case event[:when][:object]
		when 'timespan'
			start_time = Time.at(event[:when][:start_time]).strftime("%d/%m/%Y at %H:%M:%S")
			end_time = Time.at(event[:when][:end_time]).strftime("%d/%m/%Y at %H:%M:%S")
			event_date = "The time of the event is from: #{start_time} to #{end_time}"
		when 'datespan'
			start_time = event[:when][:start_date]
			end_time = event[:when][:end_date]
			event_date = "The date of the event is from: #{start_time} to: #{end_time}"
		when 'date'
			start_time = event[:when][:date]
			event_date = "The date of the event is: #{start_time}"
		end
		event[:participants].each {|participant|
			participant_details += "Email: #{participant[:email]} " \
			"Name: #{participant[:name]} Status: #{participant[:status]} - "
		}
		print "Id: #{event[:id]} | Title: #{event[:title]} | #{event_date} | " 
		puts "Participants: #{participant_details.chomp(' - ')}"
		puts "\n"
}

```

```java [listEvents-Java SDK]

import com.nylas.NylasClient;
import com.nylas.models.When;

import com.nylas.models.*;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Objects;

public class read_calendar_events {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();

    // Build the query parameters to filter our the results
    ListEventQueryParams listEventQueryParams = new ListEventQueryParams.Builder("<CALENDAR_ID>").build();

    // Read the events from our main calendar
    List<Event> events = nylas.events().list("<NYLAS_GRANT_ID>", listEventQueryParams).getData();

    for (Event event : events) {
      System.out.print("Id: " + event.getId() + " | ");
      System.out.print("Title: " + event.getTitle());

      // Dates are handled differently depending on the event type
      switch (Objects.requireNonNull(event.getWhen().getObject()).getValue()) {
        case "datespan" -> {
          When.Datespan date = (When.Datespan) event.getWhen();

          System.out.print(" | The date of the event is from: " + 
              date.getStartDate() + " to " + 
              date.getEndDate());
        }
        case "date" -> {
          When.Date date = (When.Date) event.getWhen();
          
          System.out.print(" | The date of the event is: " +date.getDate());
        }
        case "timespan" -> {
          When.Timespan timespan = (When.Timespan) event.getWhen();

          String initDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").
          format(new java.util.Date((timespan.getStartTime() * 1000L)));

          String endDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").
          format(new java.util.Date((timespan.getEndTime() * 1000L)));

          System.out.print(" | The time of the event is from: " + 
          initDate + " to " + endDate);
        }
      }

      System.out.print(" | Participants: ");

      for(Participant participant : event.getParticipants()){
        System.out.print(" Email: " + participant.getEmail() +
            " Name: " + participant.getName() +
            " Status: " + participant.getStatus());
      }

      System.out.println("\n");
    }
  }
}

```

```kt [listEvents-Kotlin SDK]

import com.nylas.NylasClient
import com.nylas.models.*

import java.util.*

fun main(args: Array<String>) {
  val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")

  val eventquery: ListEventQueryParams = ListEventQueryParams(calendarId = "<CALENDAR_ID>")

  // Get a list of events
  val myevents: List<Event> = nylas.events().list(
      "<NYLAS_GRANT_ID>", 
      queryParams = eventquery).data

  // Loop through the events
  for(event in myevents){
    print("Id: " + event.id + " | ");
    print("Title: " + event.title);

    // Get the details of Date and Time of each event.
    when(event.getWhen().getObject().toString()) {
      "DATE" -> {
        val datespan = event.getWhen() as When.Date

        print(" | The date of the event is: " + datespan.date);
      }
      "DATESPAN" -> {
        val datespan = event.getWhen() as When.Datespan

        print(" | The date of the event is: " + datespan.startDate);
      }
      "TIMESPAN" -> {
        val timespan = event.getWhen() as When.Timespan
        val startDate = Date(timespan.startTime.toLong() * 1000)
        val endDate = Date(timespan.endTime.toLong() * 1000)

        print(" | The time of the event is from: $startDate to $endDate");
      }
    }

    print(" | Participants: ");

    // Get a list of the event participants
    val participants = event.participants

    // Loop through and print their email, name and status
    for(participant in participants) {
      print(" Email: " + participant.email + " Name: " + participant.name +
          " Status: " + participant.status)
    }
    
    println("\n")
  }
}

```


For iCloud accounts, replace `<CALENDAR_ID>` in these samples with an actual calendar ID from the [List Calendars](/docs/reference/api/calendar/get-all-calendars/) response. The `primary` shortcut that works for Google and Microsoft is not available.

## Filter events

You can narrow results with query parameters. Here's the full set supported by the Events API:


| Parameter       | What it does                                 | Example                            |
| --------------- | -------------------------------------------- | ---------------------------------- |
| `calendar_id`   | **Required.** Filter by calendar             | `?calendar_id=primary`             |
| `title`         | Match on event title (case insensitive)      | `?title=standup`                   |
| `description`   | Match on description (case insensitive)      | `?description=quarterly`           |
| `location`      | Match on location (case insensitive)         | `?location=Room%20A`               |
| `start`         | Events starting at or after a Unix timestamp | `?start=1706000000`                |
| `end`           | Events ending at or before a Unix timestamp  | `?end=1706100000`                  |
| `attendees`     | Filter by attendee email (comma-delimited)   | `?attendees=alex@example.com`      |
| `busy`          | Filter by busy status                        | `?busy=true`                       |
| `metadata_pair` | Filter by metadata key-value pair            | `?metadata_pair=project_id:abc123` |


> **Warn:** 
> **iCloud does not support several filter parameters.** The following will either be ignored or return an error:
> 
> - `attendees` - not supported on CalDAV
> - `busy` - not supported on CalDAV
> - `metadata_pair` - not supported on CalDAV
> 
> Additionally, the time range between `start` and `end` cannot exceed **one year**. Requests with a wider range will fail.

Here's how to filter events by title within a time range:


```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=primary&title=standup&start=1706000000&end=1706100000&limit=10" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [filterEvents-Node.js SDK]
const events = await nylas.events.list({
  identifier: grantId,
  queryParams: {
    calendarId: "primary",
    title: "standup",
    start: 1706000000,
    end: 1706100000,
    limit: 10,
  },
});
```

```python [filterEvents-Python SDK]
events = nylas.events.list(
    grant_id,
    query_params={
        "calendar_id": "primary",
        "title": "standup",
        "start": 1706000000,
        "end": 1706100000,
        "limit": 10,
    }
)
```


Remember to replace `calendar_id=primary` in the shared examples with an actual iCloud calendar ID.

## Things to know about iCloud

iCloud Calendar runs on CalDAV, which gives it a different behavior profile than Google or Microsoft. Here's what you should plan for.

### No `primary` calendar shortcut

Google and Microsoft both support `calendar_id=primary` as a shorthand for the user's default calendar. iCloud does not. You must call the [List Calendars endpoint](/docs/reference/api/calendar/get-all-calendars/) first and pick the correct calendar ID from the response.

The default calendar name varies by language and region. English accounts typically have a calendar called "Calendar" or "Home", but don't count on it. Always discover calendar IDs dynamically.

### One-year maximum time range

The `start` to `end` range in a List Events request cannot exceed one year for iCloud accounts. If you need events across a longer window, split the request into multiple calls with consecutive one-year ranges and paginate through each.

### Limited filter support

CalDAV supports a narrower set of query capabilities than Google Calendar or Microsoft Graph. For iCloud accounts:

- `title`, `description`, `location`, `start`, and `end` work as expected
- `attendees`, `busy`, and `metadata_pair` are **not supported**
- There is no equivalent to Google's `event_type` filter
- `show_cancelled`, `ical_uid`, `updated_after`, `updated_before`, and `master_event_id` are not available

If your app targets multiple providers, test your filter combinations against iCloud specifically. A query that works on Google may silently return different results on iCloud.

### App-specific passwords and user experience

The biggest friction point for iCloud integration is the app-specific password. A few things to keep in mind:

- Passwords cannot be generated programmatically. Every user must create one manually.
- Users can revoke an app password at any time through their Apple ID settings, which invalidates the grant.
- There's no way for your app to detect a revoked password before the next sync attempt fails. Use [webhooks](/docs/v3/notifications/) to catch `grant.expired` events.
- Your onboarding flow should include step-by-step instructions with screenshots showing users how to create an app password.

### CalDAV-based sync

iCloud Calendar's CalDAV foundation means a simpler feature set compared to Google or Microsoft:

- **No conferencing auto-attach.** Google can automatically generate Meet links when you create events. iCloud has no equivalent. You can still include conferencing details manually in the event body.
- **No event types.** Google supports `focusTime`, `outOfOffice`, and `workingLocation` event types. iCloud treats all events the same.
- **No color IDs.** Calendar and event colors are managed locally in Apple Calendar and are not exposed through CalDAV.
- **Recurring events** work through standard iCalendar (RFC 5545) recurrence rules. Nylas expands recurring events into individual instances, just like it does for other providers.

### Sync timing

CalDAV sync can be slower than Google's push notifications or Microsoft's change subscriptions. A few practical notes:

- Changes made in Apple Calendar may take a few minutes to appear through the Nylas API.
- Use [webhooks](/docs/v3/notifications/) rather than polling. Nylas monitors for changes and sends notifications when events are created, updated, or deleted.
- If you need near-real-time sync and iCloud is your only provider, be aware that CalDAV introduces inherent latency that does not exist with Google or Microsoft.

### The read-only Birthdays calendar

iCloud creates a **Birthdays** calendar automatically from each contact's birthday field. It appears in your [List Calendars](/docs/reference/api/calendar/get-all-calendars/) response with `read_only: true`, and every entry is an all-day event that repeats every 12 months. Create, update, and delete requests against it fail, so filter it out by calendar ID when you build a writable calendar picker or a sync loop.

## Paginate through results


The Events API returns paginated responses. When there are more results, the response includes a `next_cursor` value. Pass it back as `page_token` to get the next page:

```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=primary&limit=10&page_token=<NEXT_CURSOR>" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [paginateEvents-Node.js SDK]
let pageCursor = undefined;

do {
  const result = await nylas.events.list({
    identifier: grantId,
    queryParams: {
      calendarId: "primary",
      limit: 10,
      pageToken: pageCursor,
    },
  });

  // Process result.data here

  pageCursor = result.nextCursor;
} while (pageCursor);
```

```python [paginateEvents-Python SDK]
page_cursor = None

while True:
    query = {"calendar_id": "primary", "limit": 10}
    if page_cursor:
        query["page_token"] = page_cursor

    result = nylas.events.list(grant_id, query_params=query)

    # Process result.data here

    page_cursor = result.next_cursor
    if not page_cursor:
        break
```

Keep paginating until the response comes back without a `next_cursor`.


For iCloud accounts, replace `calendar_id=primary` in the pagination examples with the actual calendar ID from the List Calendars response.

## What's next

- [List Google calendar events](/docs/cookbook/calendar/events/list-events-google/) for the same task on Google accounts
- [List Microsoft 365 calendar events](/docs/cookbook/calendar/events/list-events-microsoft/) for the same task on Microsoft accounts
- [Events API reference](/docs/reference/api/events/) for full endpoint documentation and all available parameters
- [Using the Events API](/docs/v3/calendar/using-the-events-api/) for creating, updating, and deleting events
- [Recurring events](/docs/v3/calendar/recurring-events/) for expanding and managing recurring event series
- [Availability](/docs/v3/calendar/calendar-availability/) for checking free/busy status across calendars
- [Webhooks](/docs/v3/notifications/) for real-time notifications instead of polling
- [iCloud provider guide](/docs/provider-guides/icloud/) for full iCloud setup including authentication
- [App passwords guide](/docs/provider-guides/app-passwords/) for generating app-specific passwords for iCloud and other providers