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 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.
Why use Nylas instead of CalDAV directly?
Section titled “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 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
Section titled “Before you begin”You’ll need:
- A Nylas application with a valid API key
- A grant for an iCloud account using the iCloud connector (not generic IMAP)
- An iCloud connector configured in your Nylas application
New to Nylas? Start with the quickstart guide to set up your app and connect a test account before continuing here.
App-specific passwords
Section titled “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:
- Go to appleid.apple.com and sign in
- Navigate to Sign-In and Security then App-Specific Passwords
- Generate a new app password
- Use that password (not their regular iCloud password) when authenticating
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 and the app passwords guide.
List events
Section titled “List events”Make a List Events request with the grant ID and a calendar_id. By default, Nylas returns up to 50 events sorted by start time.
iCloud does not support calendar_id=primary. You must call the List Calendars endpoint first to get the actual calendar ID for the account.
curl --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, application/gzip' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/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": { "name": "" }, "description": null, "grant_id": "1e3288f6-124e-405d-a13a-635a2ee54eb2", "hide_participants": false, "html_link": "https://www.google.com/calendar/event?eid=NmE0dXIwabQAAAA", "id": "6aaaaaaame8kpgcid6hvd", "object": "event", "organizer": { "name": "" }, "participants": [ { "status": "yes" }, { "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" } }]}import 'dotenv/config'import Nylas from 'nylas'
const NylasConfig = { apiKey: process.env.NYLAS_API_KEY, apiUri: process.env.NYLAS_API_URI}
const nylas = new Nylas(NylasConfig)
async function fetchAllEventsFromCalendar() { try { const events = await nylas.events.list({ identifier: process.env.NYLAS_GRANT_ID, queryParams: { calendarId: process.env.CALENDAR_ID, } })
console.log('Events:', events) } catch (error) { console.error('Error fetching calendars:', error) }}
fetchAllEventsFromCalendar()from dotenv import load_dotenvload_dotenv()
import osimport sysfrom nylas import Client
nylas = Client( os.environ.get('NYLAS_API_KEY'), os.environ.get('NYLAS_API_URI'))
grant_id = os.environ.get("NYLAS_GRANT_ID")
events = nylas.events.list( grant_id, query_params={ "calendar_id": os.environ.get("CALENDAR_ID") })
print(events)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 timeevents, _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"}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"); } }}import com.nylas.NylasClientimport 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.name + " Name: " + participant.email + " Status: " + participant.status) }
println("\n") }}For iCloud accounts, replace <CALENDAR_ID> in these samples with an actual calendar ID from the List Calendars response. The primary shortcut that works for Google and Microsoft is not available.
Filter events
Section titled “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) | [email protected] |
busy | Filter by busy status | ?busy=true |
metadata_pair | Filter by metadata key-value pair | ?metadata_pair=project_id:abc123 |
iCloud does not support several filter parameters. The following will either be ignored or return an error:
attendees- not supported on CalDAVbusy- not supported on CalDAVmetadata_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:
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>'const events = await nylas.events.list({ identifier: grantId, queryParams: { calendarId: "primary", title: "standup", start: 1706000000, end: 1706100000, limit: 10, },});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
Section titled “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
Section titled “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 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
Section titled “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
Section titled “Limited filter support”CalDAV supports a narrower set of query capabilities than Google Calendar or Microsoft Graph. For iCloud accounts:
title,description,location,start, andendwork as expectedattendees,busy, andmetadata_pairare not supported- There is no equivalent to Google’s
event_typefilter show_cancelled,ical_uid,updated_after,updated_before, andmaster_event_idare 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
Section titled “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 to catch
grant.expiredevents. - Your onboarding flow should include step-by-step instructions with screenshots showing users how to create an app password.
CalDAV-based sync
Section titled “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, andworkingLocationevent 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
Section titled “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 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.
Paginate through results
Section titled “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:
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>'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);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: breakKeep 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
Section titled “What’s next”- Events API reference for full endpoint documentation and all available parameters
- Using the Events API for creating, updating, and deleting events
- Recurring events for expanding and managing recurring event series
- Availability for checking free/busy status across calendars
- Webhooks for real-time notifications instead of polling
- iCloud provider guide for full iCloud setup including authentication
- App passwords guide for generating app-specific passwords for iCloud and other providers