---
title: Capacitor Calendar Permissions on iOS 17+ and Android
description: Which calendar permission does your Capacitor app need? Map iOS 17 write-only and full access and Android READ_CALENDAR and WRITE_CALENDAR to each method.
date:
  created: 2026-10-14
  updated: 2026-10-14
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor Calendar: sdks/capacitor/calendar.md
faq: true
---

# Capacitor Calendar Permissions on iOS 17+ and Android

Capacitor calendar permissions depend on what your app does with the calendar, and since iOS 17 the answer ranges from no permission at all to full access to every event on the device. An app that hands the new event to the system editor needs nothing, an app that writes events itself needs write-only or full access, and an app that reads events needs full access, because Apple offers no read-only level. This post maps each method of the [Capacitor Calendar plugin](../../sdks/capacitor/calendar.md) to the permission it needs on iOS 17 and newer, on iOS 16, and on Android, and explains the `prompt` state that confuses apps after a write-only grant. The plugin is part of [Capawesome Insiders](../../insiders/index.md).

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Try Capawesome Cloud free for 14 days — iOS builds without a Mac" src="https://capawesome.io/assets/banners/cloud-free-trial-capacitor-apps.png" />
  </a>
</div>

**Key takeaways:**

- `displayCreateEvent(...)` adds an event with no calendar permission on iOS 17 and on Android, because the system editor runs outside your app's process.
- iOS 17 has two access levels, write-only and full; reading needs full access, and the plugin's own write methods need full access too, because they look up the calendar first.
- Android splits the calendar group into `READ_CALENDAR` and `WRITE_CALENDAR`; the plugin needs both for `createEvent(...)`, `updateEventById(...)` and `deleteEventById(...)`.
- After a write-only grant, `checkPermissions()` reports `writeCalendar: 'granted'` and `readCalendar: 'prompt'`, and iOS shows the upgrade to full access only once.
- iOS 17 denies every request automatically when `Info.plist` carries only the legacy `NSCalendarsUsageDescription` key.

## Three access levels

Calendar access on iOS 17 and newer comes in three levels, and Apple's [EventKit guide](https://developer.apple.com/documentation/eventkit/accessing-calendar-using-eventkit-and-eventkitui){:target="_blank"} asks apps to request the lowest one that does the job. The first level is no access: the system event editor "renders its content out of process", so it can save to any calendar while your app sees nothing. The second is write-only access, which "lets your app create new events but doesn't let it read any events or other calendar information, including events your app created". The third is full access, which covers reading, editing and deleting across all calendars. There is no read-only level; Apple's documentation states that "to read events or reminders from the event store, your app needs full access."

iOS 16 and older know a single `authorized` state that covers reading and writing, requested with one usage description. On iOS 17, the old request method no longer prompts and "immediately calls the completion block with an error", which is why plugins and apps had to adopt the new levels.

Android has used two runtime permissions in the `CALENDAR` group since Android 6: `READ_CALENDAR` and `WRITE_CALENDAR`, both at the dangerous protection level, which means a system dialog at runtime. The [calendar provider guide](https://developer.android.com/identity/providers/calendar-provider){:target="_blank"} also offers the no-permission path: an `INSERT` intent hands the event to the Calendar app, and "your application doesn't even need to have the WRITE_CALENDAR permission included in its manifest file."

## Feature matrix

The Capacitor Calendar plugin exposes the three levels through two permission types, `readCalendar` and `writeCalendar`, and each method checks what it needs before it runs. The table lists the requirement per method, taken from the plugin's native code rather than from the platform rules alone:

| Method | iOS 17+ | iOS 16 | Android |
| --- | --- | --- | --- |
| `displayCreateEvent(...)` | None | None in the plugin | None |
| `displayUpdateEventById(...)` | Full access | `authorized` | None |
| `openCalendar()`, `openSettings()`, `checkPermissions()` | None | None | None |
| `getCalendars()`, `getDefaultCalendar()`, `getEvents(...)`, `getEventById(...)` | Full access | `authorized` | `READ_CALENDAR` |
| `createEvent(...)`, `updateEventById(...)`, `deleteEventById(...)` | Full access | `authorized` | `READ_CALENDAR` and `WRITE_CALENDAR` |
| `createCalendar(...)`, `deleteCalendarById(...)` | Full access | `authorized` | `WRITE_CALENDAR` |
| `calendarChange` listener | No check | No check | Inactive until `READ_CALENDAR` is granted |

Every method that needs access requests it when it is called, so a `createEvent(...)` without a prior `requestPermissions(...)` shows the system dialog on its own. The Android rule that writes also need `READ_CALENDAR` is a plugin rule, not an Android one: the plugin looks up the target calendar or the existing event before it writes, and that lookup is a read. Only the two calendar methods write without a lookup. On iOS, the same lookup is why write-only access is not enough for the plugin's own write methods, which the next sections cover.

The `calendarChange` listener behaves differently per platform. On Android, the plugin registers its content observer only once `READ_CALENDAR` is granted and starts it automatically after any permission callback; a listener added before that stays silent without an error. On iOS, the plugin observes the event store's change notification without a permission check; whether notifications arrive without full access is not documented.

## Adding without permission

The [`displayCreateEvent(...)`](../../sdks/capacitor/calendar.md#displaycreateevent) method of the Capacitor Calendar plugin adds an event without any calendar permission on iOS 17 and on Android, and Apple's migration note [TN3152](https://developer.apple.com/documentation/technotes/tn3152-migrating-to-the-latest-calendar-access-levels){:target="_blank"} makes it the first choice: "If your app can present ekeventeditviewcontroller to let people create events, don't request access to events."

```typescript
import { Calendar } from '@capawesome-team/capacitor-calendar';

const addAppointment = async () => {
  await Calendar.displayCreateEvent({
    event: {
      title: 'Dentist',
      location: 'Main Street 12',
      startDate: new Date('2026-11-03T09:00:00').getTime(),
      endDate: new Date('2026-11-03T09:30:00').getTime(),
    },
  });
};
```

On iOS, the plugin presents `EKEventEditViewController`, prefilled with your values, and the user picks the calendar, adjusts the fields and saves. On Android, it fires an `ACTION_INSERT` intent for the events content URI, which opens the Calendar app's editor the same way. Neither path touches the plugin's permission checks, so an app whose only calendar feature is "add this appointment" ships without a usage description and without manifest permissions.

The price is the result. Android returns nothing about what the user saved. On iOS, the plugin passes on the identifier of the saved event when the editor reports a save, but Apple notes that inspecting the dismissed editor "doesn't return any useful information" because the edit happens out of process, so do not build on that `id` without full access. If your app has to know the event afterwards, for example to update or delete it later, it needs full access and `createEvent(...)` instead.

## Requesting write-only

Write-only access on iOS 17 is what the Capacitor Calendar plugin requests when you ask for `writeCalendar` alone, and it needs the `NSCalendarsWriteOnlyAccessUsageDescription` key in `Info.plist`:

```typescript
import { Calendar } from '@capawesome-team/capacitor-calendar';

const requestWriteOnlyAccess = async () => {
  const status = await Calendar.requestPermissions({
    permissions: ['writeCalendar'],
  });
  return status.writeCalendar === 'granted';
};
```

After the user grants it, [`checkPermissions()`](../../sdks/capacitor/calendar.md#checkpermissions) returns `writeCalendar: 'granted'` and `readCalendar: 'prompt'`. The `prompt` is not a bug and not "never asked": the plugin maps Apple's `writeOnly` status to `granted` for writing and keeps reading at `prompt` because full access can still be requested. Apps that read `prompt` as "ask now" and call `requestPermissions()` in a loop get stuck here, which the next section explains.

Write-only access does less than its name suggests for this plugin. Apple grants it the ability to save events, but every write method of the plugin starts with a lookup: [`createEvent(...)`](../../sdks/capacitor/calendar.md#createevent) resolves the target calendar, and the update and delete methods fetch the event by its identifier. Under write-only access, Apple's documentation says a request for the calendar list "returns a single virtual calendar, that doesn't represent any real calendar in the event store", and event queries return nothing. The plugin therefore treats write-only as insufficient and requests full access when one of those methods is called. If the user declines that upgrade, the method rejects with `PERMISSION_DENIED` and the message "writeCalendar permission denied.", although `checkPermissions()` reported the write permission as granted a moment earlier.

Request write-only access anyway when your product needs it: it is the honest level for an app that saves events through other means, such as native code of your own, and it is what the user sees in Settings. For the plugin's methods, plan for full access.

## Requesting full access

Full access is what the Capacitor Calendar plugin requests by default, with both permission types or with `readCalendar` alone, and it needs `NSCalendarsFullAccessUsageDescription` on iOS 17. On Android, the same call requests `READ_CALENDAR` and `WRITE_CALENDAR` together.

The upgrade from write-only to full access happens once. Apple's [`requestFullAccessToEvents`](https://developer.apple.com/documentation/eventkit/ekeventstore/requestfullaccesstoevents(completion:)){:target="_blank"} documentation states that "the operating system only prompts them the first time your app requests full access to events", and the technote on the same change adds that after a declined upgrade "any further calls don't prompt the user for access again." The status stays `writeOnly`, `readCalendar` stays `prompt`, and the only path to full access runs through Settings > Privacy & Security > Calendars. A helper that treats a `prompt` after a request as "send to settings" avoids the loop:

```typescript
import { Calendar } from '@capawesome-team/capacitor-calendar';

const ensureFullAccess = async () => {
  let status = await Calendar.checkPermissions();
  if (status.readCalendar === 'granted') {
    return true;
  }
  status = await Calendar.requestPermissions();
  if (status.readCalendar === 'prompt-with-rationale') {
    await showCalendarRationale();
    status = await Calendar.requestPermissions();
  }
  if (status.readCalendar === 'granted') {
    return true;
  }
  await Calendar.openSettings();
  return false;
};
```

The helper opens the settings page both after a `denied` and after a `prompt` that survived a request, because on iOS both mean the system will not ask again. The `prompt-with-rationale` branch exists for Android, where `requestPermissions()` returns that state when the user denied once and the system wants an explanation first; the helper shows its own rationale and requests a second time before it falls back to [`openSettings()`](../../sdks/capacitor/calendar.md#opensettings).

Time the request with user intent. Apple's privacy session from WWDC23 expects that "full access prompts will be denied more often", and a request that appears when the user taps "Show my week" has a better chance than one at app start. Declare the manifest permissions on Android only when you call the methods that need them: an app that only displays the system editor declares neither, and an app that only creates and deletes its own calendar declares `WRITE_CALENDAR` alone.

## Info.plist keys

The Capacitor Calendar plugin reads three usage description keys, and the iOS version decides which one applies:

```xml
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Your appointments are shown next to your calendar events.</string>
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
<string>Appointments you book are added to your calendar.</string>
<key>NSCalendarsUsageDescription</key>
<string>Appointments you book are added to your calendar.</string>
```

`NSCalendarsFullAccessUsageDescription` is required on iOS 17 and newer whenever `readCalendar` is requested, and by every method that reads or modifies events. `NSCalendarsWriteOnlyAccessUsageDescription` is required on iOS 17 and newer only if you request `writeCalendar` alone. `NSCalendarsUsageDescription` serves iOS 16 and older, where the single `authorized` level still applies. When a required key is missing, `requestPermissions(...)` rejects with the message "One or more privacy descriptions are missing." instead of showing a dialog.

Keeping only the legacy key is the mistake that produces silent denials. Apple's [event store documentation](https://developer.apple.com/documentation/eventkit/accessing-the-event-store){:target="_blank"} is explicit: "if your app doesn't include usage description keys, or only includes the older NSCalendarsUsageDescription key for describing events access, iOS automatically denies any access request." A Capacitor app that was last touched before iOS 17 and still ships the old key alone fails every request on current devices while working on an iOS 16 test device.

One more key may matter on older systems. Apple's documentation notes that EventKit UI "may need to access Contacts data" on iOS 10 through 16 and that apps without `NSContactsUsageDescription` crash there when they present the editor. The plugin's documentation does not list the key; add it if you support iOS 16 and call `displayCreateEvent(...)`.

## FAQ

### Do I need calendar read permission to add an event on iOS 17?

Not if you use the system editor. `displayCreateEvent(...)` of the [Capacitor Calendar plugin](../../sdks/capacitor/calendar.md) opens Apple's event editor, which saves without any permission on iOS 17 and newer. If you create the event in code with `createEvent(...)`, the plugin needs full access, because it looks up the target calendar first, and write-only access returns no calendars. On Android, `createEvent(...)` needs `READ_CALENDAR` and `WRITE_CALENDAR`, while `displayCreateEvent(...)` needs neither.

### Why does `readCalendar` stay `prompt` after the user granted access?

Because the user granted write-only access. The plugin reports write-only as `writeCalendar: 'granted'` and `readCalendar: 'prompt'`, since full access can still be requested. iOS shows that upgrade prompt once; after a decline, `readCalendar` stays `prompt` and only the Settings app can change it, so route the user to `openSettings()` instead of requesting again.

### Does Android need READ_CALENDAR to write an event?

For the plugin's `createEvent(...)`, `updateEventById(...)` and `deleteEventById(...)`, yes, because they look up the calendar or the event before writing. `createCalendar(...)` and `deleteCalendarById(...)` work with `WRITE_CALENDAR` alone, and `displayCreateEvent(...)` works without any permission.

### What happens to users who granted calendar access on iOS 16?

Apple said at WWDC23 that an app with existing calendar access "defaults to write-only permission on upgrade to iOS 17", and its technote documents that behavior for apps built against older SDKs. Test the upgrade path with a device that moves from iOS 16 to 17 before you rely on existing grants, and make sure the app handles `readCalendar: 'prompt'` with the settings fallback above.

### Is the Capacitor Calendar plugin free?

No. The plugin is part of the [Capawesome Insiders](../../insiders/index.md) subscription and is installed from the Capawesome npm registry with the license key that comes with it.

## Related posts

- [Announcing the Capacitor Calendar Plugin](./announcing-the-capacitor-calendar-plugin.md)
- [How to Manage Recurring Calendar Events in Capacitor](./recurring-calendar-events-in-capacitor.md)
- [Android Scoped Storage in Capacitor Apps, Explained](./android-scoped-storage-in-capacitor-apps.md)

## Try Capawesome Cloud

Changing a usage description or a manifest permission is a native change, and a wrong `Info.plist` key only shows up on a real iOS 17 device. Capawesome Cloud builds and signs your app for Android and iOS without a Mac, so you can test the permission flow on a device minutes after the change. [How to Sign & Build Capacitor Apps in the Cloud](./how-to-sign-and-build-your-capacitor-app-in-the-cloud.md) shows the steps.

[Try Capawesome Cloud Free](https://capawesome.io){ .md-button .md-button--primary }

## Conclusion

Pick the level by the feature: the system editor for "add this to my calendar", full access for anything that reads events or that uses `createEvent(...)`, and write-only only when your own native code saves events. Ship all three `Info.plist` keys when you support iOS 16, request full access from a user action rather than at launch, and treat a `prompt` that survives a request as a trip to Settings. For what to do once the permission is in place, [How to Manage Recurring Calendar Events in Capacitor](./recurring-calendar-events-in-capacitor.md) covers the harder part of the API.

If you have any questions, join us on the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}. To stay updated on the latest news, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"}.
