Apple has no public calendar REST API. Creating events on iCloud Calendar natively means constructing iCalendar (ICS) payloads wrapped in XML envelopes and sending them over CalDAV, a protocol that requires persistent connections and manual credential management. Nylas handles all of that for you and exposes iCloud Calendar through the same Events API you use for Google and Microsoft.
This guide walks through creating events on iCloud Calendar accounts, including the app-specific password requirement, participant notification behavior, and the iCloud-specific limitations you should plan around.
Why use Nylas instead of CalDAV directly?
Section titled “Why use Nylas instead of CalDAV directly?”CalDAV is functional, but building event creation on it directly comes with real costs:
- ICS format construction. Creating an event means building a valid iCalendar object with correct VTIMEZONE blocks, VEVENT properties, and RRULE syntax, all wrapped in a CalDAV PUT request. Nylas gives you a JSON body and a single POST endpoint.
- No conferencing auto-create. Google can generate Meet links automatically when you create an event. Microsoft can attach Teams links. CalDAV has nothing comparable. You would need to integrate with a conferencing provider separately.
- No room resources. CalDAV does not support the concept of room or resource booking. If your app needs meeting rooms, iCloud cannot provide them natively.
- No programmatic password generation. Every user must manually create an app-specific password through their Apple ID settings. This step cannot be automated.
- Connection management. You need to maintain CalDAV sessions per user, handle reconnections, and manage sync state yourself. Nylas does this behind the scenes.
If you only target iCloud and are comfortable with iCalendar format, CalDAV works. For multi-provider apps or faster development, Nylas removes the protocol-level complexity entirely.
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.
Create an event
Section titled “Create an event”Make a Create Event request with the grant ID and a calendar_id query parameter. Nylas creates the event on the specified calendar and returns the new event object with its id.
iCloud does not support calendar_id=primary. You must call the List Calendars endpoint first to get the actual calendar ID for the account. The default calendar name varies by language and region, so always discover calendar IDs dynamically.
You’re about to send a real event invite! The following samples send an email from the account you connected to the Nylas API to any email addresses in the participants sub-object. Make sure you actually want to invite those addresses before running this request.
curl --request POST \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>' \ --header 'Accept: application/json, application/gzip' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json' \ --data '{ "title": "Annual Philosophy Club Meeting", "busy": true, "conferencing": { "provider": "Zoom Meeting", "autocreate": { "conf_grant_id": "<NYLAS_GRANT_ID>", "conf_settings": { "settings": { "join_before_host": true, "waiting_room": false, "mute_upon_entry": false, "auto_recording": "none" } } } } "participants": [ { "name": "Leyah Miller", "email": "[email protected]" }, { "name": "Nyla", "email": "[email protected]" } ], "resources": [{ "name": "Conference room", "email": "[email protected]" }], "description": "Come ready to talk philosophy!", "when": { "start_time": 1674604800, "end_time": 1722382420, "start_timezone": "America/New_York", "end_timezone": "America/New_York" }, "location": "New York Public Library, Cave room", "recurrence": [ "RRULE:FREQ=WEEKLY;BYDAY=MO", "EXDATE:20211011T000000Z" ],}'{ "request_id": "1", "data": { "busy": true, "calendar_id": "primary", "conferencing": { "details": { "meeting_code": "<MEETING_CODE>", "url": "<MEETING_URL>" }, "provider": "Google Meet" }, "created_at": 1701974804, "creator": { "name": "Leyah Miller" }, "description": null, "grant_id": "<NYLAS_GRANT_ID>", "hide_participants": false, "html_link": "<EVENT_LINK>", "id": "<NYLAS_EVENT_ID>", "object": "event", "organizer": { "name": "Leyah Miller" }, "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)
const now = Math.floor(Date.now() / 1000) // Time in Unix timestamp format (in seconds)
async function createAnEvent() { try { const event = await nylas.events.create({ identifier: process.env.NYLAS_GRANT_ID, requestBody: { title: 'Build With Nylas', when: { startTime: now, endTime: now + 3600, } }, queryParams: { calendarId: process.env.CALENDAR_ID, }, })
console.log('Event:', event); } catch (error) { console.error('Error creating event:', error) }}
createAnEvent()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.create( grant_id, request_body={ "title": 'Build With Nylas', "when": { "start_time": 1609372800, "end_time": 1609376400 }, }, 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: "<NYLAS_GRANT_ID>"}
today = Date.todaystart_time = Time.local(today.year, today.month, today.day, 13, 0, 0).strftime("%s")end_time = Time.local(today.year, today.month, today.day, 13, 30, 0).strftime("%s")
request_body = { when: { start_time: start_time.to_i, end_time: end_time.to_i }, title: "Let's learn some Nylas Ruby SDK!", location: "Nylas' Headquarters", description: "Using the Nylas API with the Ruby SDK is easy.", participants: [{ name: "Blag", status: 'noreply' }]}
events, _request_ids = nylas.events.create( identifier: "<NYLAS_GRANT_ID>", query_params: query_params, request_body: request_body)import com.nylas.NylasClient;import com.nylas.models.*;
import java.time.Instant;import java.time.LocalDate;import java.time.ZoneOffset;import java.time.temporal.ChronoUnit;import java.util.*;
public class create_calendar_events { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
// Get today's date LocalDate today = LocalDate.now();
// Set time. Because we're using UTC we need to add the hours in difference from our own timezone. Instant sixPmUtc = today.atTime(13, 0).toInstant(ZoneOffset.UTC);
// Set the date and time for the event. We add 30 minutes to the starting time. Instant sixPmUtcPlus = sixPmUtc.plus(30, ChronoUnit.MINUTES);
// Get the Date and Time as a Unix timestamp long startTime = sixPmUtc.getEpochSecond(); long endTime = sixPmUtcPlus.getEpochSecond();
// Define title, location, and description of the event String title = "Let's learn some about the Nylas Java SDK!"; String location = "Nylas Headquarters"; String description = "Using the Nylas API with the Java SDK is easy.";
// Create the timespan for the event CreateEventRequest.When.Timespan timespan = new CreateEventRequest. When.Timespan. Builder(Math.toIntExact(startTime), Math.toIntExact(endTime)). build();
// Create the list of participants. List<CreateEventRequest.Participant> participants_list = new ArrayList<>();
participants_list.add(new CreateEventRequest. "John Doe", "", ""));
// Build the event details. CreateEventRequest createEventRequest = new CreateEventRequest.Builder(timespan) .participants(participants_list) .title(title) .location(location) .description(description) .build();
// Build the event parameters. In this case, the Calendar ID. CreateEventQueryParams createEventQueryParams = new CreateEventQueryParams.Builder("<CALENDAR_ID>").build();
// Create the event itself Event event = nylas.events().create( "<NYLAS_GRANT_ID>", createEventRequest, createEventQueryParams).getData(); }}import com.nylas.NylasClientimport com.nylas.models.*
import java.time.LocalDateTimeimport java.time.ZoneOffset
fun main(args: Array<String>) { val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>") var startDate = LocalDateTime.now()
// Set the time. Because we're using UTC, we need to add the difference in hours from our own timezone. startDate = startDate.withHour(13); startDate = startDate.withMinute(0); startDate = startDate.withSecond(0); val endDate = startDate.withMinute(30);
// Convert the dates from Unix timestamp format to integer. val iStartDate: Int = startDate.toEpochSecond(ZoneOffset.UTC).toInt() val iEndDate: Int = endDate.toEpochSecond(ZoneOffset.UTC).toInt()
// Create the timespan for the event. val eventWhenObj: CreateEventRequest.When = CreateEventRequest.When. Timespan(iStartDate, iEndDate);
// Define the title, location, and description of the event. val title: String = "Let's learn about the Nylas Kotlin/Java SDK!" val location: String = "Blag's Den!" val description: String = "Using the Nylas API with the Kotlin/Java SDK is easy."
// Create the list of participants. val participants: List<CreateEventRequest.Participant> = listOf(CreateEventRequest. Participant("<PARTICIPANT_EMAIL>", ParticipantStatus.NOREPLY, "<PARTICIPANT_NAME>"))
// Create the event request. This adds date/time, title, location, description, and participants. val eventRequest: CreateEventRequest = CreateEventRequest(eventWhenObj, title, location, description, participants)
// Set the event parameters. val eventQueryParams: CreateEventQueryParams = CreateEventQueryParams("<CALENDAR_ID>")
val event: Response<Event> = nylas.events().create(dotenv["NYLAS_GRANT_ID"], eventRequest, eventQueryParams)}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 on iCloud.
Add participants and send invitations
Section titled “Add participants and send invitations”When you include participants in your create event request and set notify_participants=true (the default), Nylas sends invitation emails to each participant. On iCloud, these invitations go out as ICS file attachments via email, which is how CalDAV handles event notifications natively.
This is simpler than the notification systems on Google and Microsoft. There are no push notifications, no in-app notification bells, and no rich invitation cards. Participants receive a standard email with an ICS attachment they can accept or decline.
When notify_participants=false, Nylas creates the event on the organizer’s calendar only. Participants do not receive an invitation email and the event does not appear on their calendars.
A few things to keep in mind:
- CalDAV sends invitations as ICS files. Some email clients render these as calendar invites with accept/decline buttons, while others show them as plain attachments.
- There is no way to customize the invitation email body through CalDAV. The content is generated automatically based on the event details.
- Participant response status (
accepted,declined,tentative) syncs back through CalDAV, but with the latency you would expect from a polling-based protocol.
Things to know about iCloud
Section titled “Things to know about iCloud”iCloud Calendar runs on CalDAV, which gives it a different feature profile than Google or Microsoft. Here’s what matters when creating events.
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 hardcode that. Always discover calendar IDs dynamically.
No conferencing auto-create
Section titled “No conferencing auto-create”Google can automatically generate Meet links when you create an event, and Microsoft can attach Teams links. iCloud has no equivalent. CalDAV does not support any conferencing integration.
If your users need a video call link on the event, you can include the URL in the location or description field manually. This works, but you will need to handle the conferencing provider integration yourself.
No room resources
Section titled “No room resources”iCloud does not support the resources field. CalDAV has no concept of room or resource booking. If your app needs meeting room scheduling alongside event creation, iCloud cannot provide it. Events that include resources in the request body will have that field ignored.
Simpler event model
Section titled “Simpler event model”CalDAV supports the core event fields and not much else. On iCloud:
title,when,participants,description,location, andrecurrenceall work as expectedevent_typeis not available (no focus time, out of office, or working location support)color_idis not exposed through CalDAV. Calendar and event colors are managed locally in the Apple Calendar appcapacityis not supported
The core fields cover most use cases. If your app depends on extended event properties, test against iCloud specifically to confirm what comes back.
All-day events
Section titled “All-day events”Use the datespan format for all-day events, the same as Google and Microsoft. Set the when object with a start_date and end_date in YYYY-MM-DD format. The end date is exclusive, so a single-day event on March 5 would use start_date: "2026-03-05" and end_date: "2026-03-06".
Recurring events
Section titled “Recurring events”Standard RRULE support works through CalDAV using iCalendar (RFC 5545) recurrence rules. Nylas expands recurring events into individual instances, just like it does for other providers. You can create recurring events by including an recurrence array in the request body.
For details on managing recurring event series, see the recurring events guide.
App-specific passwords can break
Section titled “App-specific passwords can break”If a user revokes their app-specific password through their Apple ID settings, all API calls for that grant will fail. There is no way to detect a revoked password proactively. Use webhooks to listen for grant.expired events so your app can prompt the user to re-authenticate.
Your onboarding flow should set clear expectations: if the user deletes their app password, their calendar integration stops working until they create a new one.
Sync timing
Section titled “Sync timing”CalDAV sync can be slower than Google’s push notifications or Microsoft’s change subscriptions. Events you create through the API may take a few minutes to appear in Apple Calendar apps on the user’s devices. This latency is inherent to CalDAV and not something Nylas or your app can control.
Use webhooks rather than polling to detect changes. Nylas monitors for updates and sends notifications when events are created, updated, or deleted.
What’s next
Section titled “What’s next”- Events API reference for full endpoint documentation and all available parameters
- Using the Events API for updating, deleting, and managing events
- List iCloud calendar events for retrieving events from iCloud accounts
- 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