If you searched for an “Apple Calendar API,” you probably expected a developer portal, an API key, and REST endpoints. Apple offers none of those for iCloud Calendar. What Apple does offer is EventKit, a framework that works inside apps on an iPhone, iPad, or Mac, and CalDAV, the XML-based protocol iCloud Calendar speaks to servers. This page explains what each one can do, how iCloud authentication works, and how to read iCloud events as JSON through the Nylas Calendar API.
Does Apple have a public calendar API?
Section titled “Does Apple have a public calendar API?”No. Apple has no public REST API, developer portal, or API key for iCloud Calendar. Apple’s EventKit framework reads and writes calendars, but only inside an app running on an Apple device. For server-side access, iCloud Calendar exposes CalDAV, an XML protocol over WebDAV standardized in RFC 4791 in 2007.
Nylas wraps CalDAV behind a JSON Events API that covers iCloud, Google, and Microsoft as 3 providers under one set of endpoints, so your backend never parses CalDAV XML or builds iCalendar files.
What does Apple actually offer for calendar access?
Section titled “What does Apple actually offer for calendar access?”Apple offers 2 calendar integration paths, and they target different environments. EventKit is a framework for apps on iOS, iPadOS, Mac Catalyst, macOS, watchOS, and visionOS that reads the calendars synced to that device. CalDAV is the network protocol iCloud Calendar speaks, and it’s the only option for a backend server that needs a user’s iCloud events.
CalDAV uses WebDAV extensions with XML request and response bodies, persistent connections per user, and iCalendar payloads (RFC 5545, published in 2009) for every event. There is no developer dashboard, no API key, and no way to generate credentials programmatically. Each user creates an app-specific password by hand in their Apple Account settings. CalDAV also lacks native push, so detecting changes means polling on an interval you maintain yourself. For a single-provider hobby project this is workable, but production apps also need reconnection logic, XML parsing, and sync-state management.
What about EventKit on iOS and macOS?
Section titled “What about EventKit on iOS and macOS?”EventKit is Apple’s on-device calendar framework. It runs on 6 Apple platforms, including iOS 4.0 and later and macOS 10.8 and later, and gives an app access to the calendars stored on that device, such as iCloud, Exchange, and subscribed calendars. It isn’t a web API, so a backend server can’t call it.
Before EventKit returns any events, the person using the app must grant permission on the device. Apple’s Accessing the event store guide says an app can’t request read-only access to events: it asks for write-only access or full access. On iOS 17 and later, the app must also declare NSCalendarsFullAccessUsageDescription or NSCalendarsWriteOnlyAccessUsageDescription in its Info.plist, or iOS denies the request.
EventKit fits an iPhone or Mac app that works with its own user’s calendar. It doesn’t fit a web app, a SaaS backend, or an AI agent that reads calendars for 1,000 users, because it only runs inside your app on each user’s device. For those cases, use CalDAV directly or the Nylas Calendar API, which reaches the same iCloud calendars from your server.
EventKit vs CalDAV vs the Nylas Calendar API
Section titled “EventKit vs CalDAV vs the Nylas Calendar API”The table below compares 3 ways to work with Apple calendars: EventKit inside an app, CalDAV called directly, and the Nylas Calendar API, which normalizes iCloud alongside Google and Microsoft. Across all 3 providers, it uses the same GET and POST routes, which removes most of the per-provider branching you’d otherwise write.
| Capability | EventKit (on device) | Apple CalDAV (direct) | Nylas Calendar API |
|---|---|---|---|
| Where it runs | Inside an app on an Apple device | Any server or client | Any server or client over HTTPS |
| Data format | Native objects such as EKEvent | XML over WebDAV, iCalendar bodies | JSON over REST |
| Authentication | User grants permission on the device | App-specific password, manual | Hosted or custom auth, app-specific password |
| Event creation | Save an EKEvent to the event store | Hand-built iCalendar file in a PUT | Single POST with a JSON body |
| Change notifications | Notification when the device’s data changes | None, you poll | Webhooks across every provider |
| Calendar coverage | Calendars synced to that device | iCloud only | iCloud, Google, and Microsoft, one API |
| Connection handling | Managed by the operating system | You maintain sessions per user | Managed by Nylas |
If you’re building a native Apple app, EventKit is the right tool. If you only need iCloud from a server and are comfortable parsing XML, CalDAV works. For most backend teams, the unified API removes weeks of protocol work.
The diagram below maps those three paths to where your code runs.
How to access Apple Calendar data
Diagram as text
- Where does your code run?: EventKit runs on the device. CalDAV and the Nylas Calendar API run on a server.
- Ends in one of:
- In an iOS or macOS app → Best for native apps: EventKit: Reads the calendars synced to that device, including iCloud. The user grants permission on the device. A server can't call it.
- On a server, iCloud only → Works, more to maintain: CalDAV directly: XML over WebDAV with iCalendar bodies. Each user creates an app-specific password. No push, so you poll for changes.
- On a server, multiple providers → Best for most backends: Nylas Calendar API: One REST API for iCloud, Google, and Microsoft, with webhooks for changes. iCloud users still create an app-specific password.
Does the Apple Calendar API support OAuth?
Section titled “Does the Apple Calendar API support OAuth?”Not in the way Google and Microsoft do. Sign in with Apple can request only 2 scopes, a user’s full name and email address, so it never grants calendar access. The Nylas iCloud connector signs in with the user’s Apple Account email plus an app-specific password, and that works the same from PHP, Python, JavaScript, or any other language.
Nylas supports 2 iCloud authentication flows, both documented in the iCloud provider guide:
- Hosted authentication: redirect the user to
GET /v3/connect/auth, where they sign in with their iCloud email and app-specific password. Then exchange the code atPOST /v3/connect/tokenfor a grant ID. Your app runs a standard OAuth 2.0 redirect against Nylas, so a PHP, Python, or Node.js backend uses the same code it would for Google. - Bring Your Own (BYO) Authentication: collect the email and app-specific password on your own page, then send them to
POST /v3/connect/customwith"provider": "icloud".
Apple’s support site also describes authorizing supported third-party apps with an Apple Account instead of an app-specific password. The Nylas iCloud connector uses the app-specific password.
How do app-specific passwords work for iCloud?
Section titled “How do app-specific passwords work for iCloud?”An app-specific password is a separate password a user generates in their Apple Account so a third-party app can reach iCloud Mail, Calendar, and Contacts without seeing the main password. Apple requires two-factor authentication on the account, and each account can hold up to 25 active app-specific passwords at a time.
Apple exposes no endpoint for minting these credentials, so onboarding 200 users means 200 manual password steps. Show clear instructions, and expect that a user who pastes their regular password fails authentication every time. Users can revoke a password at any moment, and Apple revokes all of them automatically when the user changes or resets their main Apple Account password. Either one silently invalidates the grant. There’s no proactive signal before the next sync fails, so listen for the grant.expired event through webhooks and prompt the user to reconnect. Apple’s app-specific password instructions list the exact steps users follow.
Read iCloud events from JavaScript, Python, or PHP
Section titled “Read iCloud events from JavaScript, Python, or PHP”You read iCloud Calendar events from any backend language with one HTTPS request: GET /v3/grants/{grant_id}/events. Nylas returns JSON, so JavaScript, Python, PHP, Ruby, or Java code needs no CalDAV client or XML parser. Each page holds 50 events by default and up to 200 with the limit parameter.
The samples below list events on one iCloud calendar. Replace <CALENDAR_ID> with an ID from List Calendars, because iCloud doesn’t accept calendar_id=primary. The start and end values are Unix timestamps in seconds, and for iCloud the range between them can’t exceed 1 year.
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'{ "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 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();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)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.email + " Name: " + participant.name + " Status: " + participant.status) }
println("\n") }}Nylas SDKs cover Node.js, Python, Ruby, Java, and Kotlin. From PHP, send the request in the curl tab with any HTTP client, such as PHP’s cURL extension, and pass your API key in the Authorization: Bearer header. The Google Contacts API PHP example shows that REST pattern in PHP, and it works the same against the Events endpoint.
How do I read and create iCloud events?
Section titled “How do I read and create iCloud events?”Once a grant exists, you read events with GET /v3/grants/{grant_id}/events?calendar_id=... and create them with POST to the same path. Both calls take a real calendar_id, because iCloud rejects the calendar_id=primary shortcut that works on Google and Microsoft, so fetch the ID first with List Calendars. Each list request returns 50 events per page by default, sorted by start time.
The two task recipes below cover filters, pagination, event creation, participant invitations, and the iCloud-specific behaviors you should plan around:
- List iCloud calendar events covers reading events, the one-year time range cap, and limited CalDAV filter support.
- Create iCloud calendar events covers writing events, participant invitations sent as ICS attachments, and the simpler event model.
What should I know before choosing CalDAV or Nylas?
Section titled “What should I know before choosing CalDAV or Nylas?”Decide based on scope. If iCloud is your only target and you accept ongoing maintenance, direct CalDAV avoids a dependency. If you support more than one provider, or want to ship in days rather than weeks, the unified API is the faster path.
Either way, remember three constraints that apply to every iCloud integration: app-specific passwords are mandatory and manual, there is no primary calendar alias, and the read range between start and end can’t exceed 1 year per request. Email and calendar both connect through the same iCloud connector, and message data is cached for 90 days. The iCloud provider guide walks through connector setup, both authentication flows, and the rate limits Apple enforces.
What’s next
Section titled “What’s next”- List iCloud calendar events for the read path with filters and pagination
- Create iCloud calendar events for the write path with full code samples
- iCloud Mail API for what Apple offers for iCloud email: IMAP, SMTP, and app-specific passwords
- Apple Contacts API for iCloud contacts over CardDAV and the on-device Contacts framework
- CalDAV vs the Nylas Calendar API for the build-or-buy decision across providers
- iCloud provider guide for connector setup and authentication
- Events API reference for every endpoint and parameter
- App passwords guide for generating app-specific passwords
- Webhooks for change notifications instead of polling