---
title: How to Add Events to the Device Calendar in an Ionic App
description: Add events to the device calendar in an Ionic app with the Capacitor Calendar plugin — open the system event dialog without permissions or write events silently.
date:
  created: 2026-11-04
  updated: 2026-11-04
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - Ionic Framework
  - SDKs
links:
  - Capacitor Calendar: sdks/capacitor/calendar.md
faq: true
---

# How to Add Events to the Device Calendar in an Ionic App

Adding an event to the device calendar from an Ionic app takes one call and, on the shortest path, no calendar permission at all. This guide builds an "Add to calendar" button for a booking page in Ionic Angular with the [Capacitor Calendar plugin](../../sdks/capacitor/calendar.md) and covers both ways to add the event: the system event dialog, which lets the user review and save the event without your app ever reading the calendar, and a silent write for apps that must update or remove the event later. It replaces the Ionic 3-era tutorials built on `cordova-plugin-calendar`, whose last release dates from January 2022. 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(...)` opens the system event editor prefilled with your data and needs no calendar permission, no `Info.plist` key and no manifest entry on Android and on iOS 17 and later.
- The Android editor takes the title, description, location, start and end, all-day flag, availability and recurrence rule from the intent; alerts, the target calendar and the time zone are ignored there.
- Only iOS reports whether the user saved the event and returns its `id`; Android returns nothing, so the dialog path suits apps that do not need the event afterwards.
- `createEvent(...)` writes the event silently and returns its `id` for later updates and deletes, but needs full access on iOS 17 and `READ_CALENDAR` plus `WRITE_CALENDAR` on Android.
- `cordova-plugin-calendar` last shipped 5.1.6 in January 2022 and `@ionic-native/calendar` stopped at 5.36.0 in August 2021; the Capacitor Calendar plugin targets Capacitor 8.

## What you will build

The page in this guide shows one booking, a dentist appointment with a start, an end and an address, and an "Add to calendar" button below it. Pressing the button opens the calendar editor of the phone with every field filled in, and the user saves the event with one tap or changes the calendar first. A second variant, for apps that keep bookings in sync with the calendar, writes the event without any dialog and remembers its identifier so that a rescheduled booking moves the event and a canceled booking removes it.

Both variants live in one `CalendarService` provided in root. The page is a standalone component, and the plugin calls are plain TypeScript, so an Ionic React or Vue app reuses the service with a different wrapper. The booking model is as small as it gets:

```typescript
export interface Booking {
  id: string;
  title: string;
  start: Date;
  end: Date;
  location: string;
  notes: string;
  calendarEventId?: string;
}
```

To install the Capacitor Calendar plugin, please refer to the [Installation](../../sdks/capacitor/calendar.md/#installation) section in the plugin documentation. [Announcing the Capacitor Calendar Plugin](./announcing-the-capacitor-calendar-plugin.md) walks through the whole API, from calendars and recurring events to the change listener; this guide stays with adding a single event.

## Two ways to add

The Capacitor Calendar plugin adds an event either through the system dialog with [`displayCreateEvent(...)`](../../sdks/capacitor/calendar.md#displaycreateevent) or directly. The direct call is [`createEvent(...)`](../../sdks/capacitor/calendar.md#createevent), and the choice between the two is about what your app needs to know afterwards:

| | System dialog | Silent write |
| --- | --- | --- |
| Permission prompt | None | Full access on iOS 17+, `READ_CALENDAR` and `WRITE_CALENDAR` on Android |
| `Info.plist` keys | None | `NSCalendarsFullAccessUsageDescription`, plus `NSCalendarsUsageDescription` for iOS 16 |
| Android manifest | Nothing | Two `uses-permission` entries |
| User can review and edit first | Yes | No |
| App learns the event `id` | iOS only, if saved | Yes, on both platforms |
| App can update or delete the event later | No | Yes |

Apple's guidance for iOS 17 points at the first column: an app that can present the system editor to create events should not request access to events at all. [Capacitor Calendar Permissions on iOS 17+ and Android](./capacitor-calendar-permissions.md) goes through the three access levels and every method's requirement; here it is enough to know that the dialog path has none and the silent path needs full access, because `createEvent(...)` looks up the target calendar before it writes.

## Open the system dialog

To add a calendar event from an Ionic app without asking for permission, pass the booking to `displayCreateEvent(...)`. The plugin presents `EKEventEditViewController` on iOS and fires an `ACTION_INSERT` intent for the calendar provider on Android, both prefilled with the event data:

```typescript
import { Injectable } from '@angular/core';
import { Calendar } from '@capawesome-team/capacitor-calendar';
import { Booking } from './booking';

@Injectable({ providedIn: 'root' })
export class CalendarService {
  async addWithDialog(booking: Booking): Promise<string | undefined> {
    const { id } = await Calendar.displayCreateEvent({
      event: {
        title: booking.title,
        startDate: booking.start.getTime(),
        endDate: booking.end.getTime(),
        location: booking.location,
        description: booking.notes,
      },
    });
    return id;
  }
}
```

`startDate` and `endDate` are timestamps in milliseconds, and `endDate` defaults to one hour after the start when it is left out. The promise resolves when the dialog closes. On iOS, `id` holds the identifier of the saved event and is absent when the user canceled. On Android, the system dialog reports nothing back to the app, so `id` is always `undefined` there and the method cannot tell a save from a cancel.

What the dialog shows depends on the platform. On iOS, the plugin applies every field of the event, so the editor opens with alerts, the chosen calendar and the time zone as well. On Android, the plugin puts only the properties the insert intent understands into the intent: the title, description and location, the begin and end time, the all-day flag, the availability and the recurrence rule. The remaining properties, `alerts`, `calendarId` and `timezone`, are silently ignored, and the user picks the calendar in the editor. If the booking needs a reminder on Android, say so in the `description` or use the silent path.

The dialog belongs to the operating system, which has two consequences. There is nothing to configure or style, and the editor looks like the one in the Calendar app the user already knows. And the plugin's documentation does not list `NSContactsUsageDescription`, which Apple says EventKit UI may need on iOS 10 through 16; the permissions post above covers when to add it.

## Write the event silently

A booking app that updates the calendar when the booking changes needs the event's identifier, and that means `createEvent(...)` with full access. The service requests the permission on first use and stores the returned `id` with the booking:

```typescript
import { Injectable } from '@angular/core';
import { Calendar } from '@capawesome-team/capacitor-calendar';
import { Booking } from './booking';

@Injectable({ providedIn: 'root' })
export class CalendarService {
  async addSilently(booking: Booking): Promise<string | null> {
    const status = await Calendar.requestPermissions();
    if (status.readCalendar !== 'granted' || status.writeCalendar !== 'granted') {
      return null;
    }
    const { id } = await Calendar.createEvent({
      event: {
        title: booking.title,
        startDate: booking.start.getTime(),
        endDate: booking.end.getTime(),
        location: booking.location,
        description: booking.notes,
        alerts: [60],
      },
    });
    return id;
  }

  async reschedule(booking: Booking): Promise<void> {
    if (!booking.calendarEventId) {
      return;
    }
    await Calendar.updateEventById({
      id: booking.calendarEventId,
      event: {
        startDate: booking.start.getTime(),
        endDate: booking.end.getTime(),
      },
    });
  }

  async remove(booking: Booking): Promise<void> {
    if (!booking.calendarEventId) {
      return;
    }
    await Calendar.deleteEventById({ id: booking.calendarEventId });
  }
}
```

[`requestPermissions()`](../../sdks/capacitor/calendar.md#requestpermissions) without options asks for both permission types, which is full access on iOS 17 and the two runtime permissions on Android. Asking for `writeCalendar` alone is not enough for `createEvent(...)`, because the plugin resolves the target calendar first, and that lookup is a read. The event goes into the default calendar when no `calendarId` is set, and `alerts: [60]` adds a reminder one hour before the start, which the operating system shows even while your app is closed. [`updateEventById(...)`](../../sdks/capacitor/calendar.md#updateeventbyid) changes only the properties you pass. [`deleteEventById(...)`](../../sdks/capacitor/calendar.md#deleteeventbyid) removes the event. Both reject with `PERMISSION_DENIED` when the user has revoked access in the meantime, so wrap them in the same permission check.

This path needs declarations on both platforms. In `ios/App/App/Info.plist`, add the full access key for iOS 17 and later and the legacy key for iOS 16 and older:

```xml
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Your bookings are added to your calendar and updated when they change.</string>
<key>NSCalendarsUsageDescription</key>
<string>Your bookings are added to your calendar and updated when they change.</string>
```

In `android/app/src/main/AndroidManifest.xml`, declare both calendar permissions:

```xml
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
```

Ship only what the path you use needs. An app with the dialog path alone declares neither, and store reviewers may ask why an app that could use the editor reads the user's calendar instead.

## The booking page

The page renders the booking and offers the dialog path as its primary action. After a silent save, [`openCalendar(...)`](../../sdks/capacitor/calendar.md#opencalendar) shows the day in the calendar app so that the user sees the result:

```typescript
import { Component, inject, signal } from '@angular/core';
import { DatePipe } from '@angular/common';
import {
  IonButton,
  IonContent,
  IonHeader,
  IonItem,
  IonLabel,
  IonList,
  IonNote,
  IonTitle,
  IonToolbar,
} from '@ionic/angular/standalone';
import { Calendar } from '@capawesome-team/capacitor-calendar';
import { Booking } from './booking';
import { CalendarService } from './calendar.service';

@Component({
  selector: 'app-booking',
  imports: [DatePipe, IonButton, IonContent, IonHeader, IonItem, IonLabel, IonList, IonNote, IonTitle, IonToolbar],
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-title>Your booking</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content class="ion-padding">
      <ion-list>
        <ion-item>
          <ion-label>{{ booking().title }}</ion-label>
          <ion-note slot="end">{{ booking().start | date: 'medium' }}</ion-note>
        </ion-item>
        <ion-item>
          <ion-label>{{ booking().location }}</ion-label>
        </ion-item>
      </ion-list>
      <ion-button expand="block" (click)="addToCalendar()">Add to calendar</ion-button>
      @if (booking().calendarEventId) {
        <ion-button expand="block" fill="outline" (click)="showInCalendar()">Show in calendar</ion-button>
      }
    </ion-content>
  `,
})
export class BookingPage {
  private readonly calendarService = inject(CalendarService);
  readonly booking = signal<Booking>({
    id: 'booking-1',
    title: 'Dentist appointment',
    start: new Date('2026-11-24T09:00:00'),
    end: new Date('2026-11-24T09:30:00'),
    location: 'Main Street 12, Springfield',
    notes: 'Bring your insurance card.',
  });

  async addToCalendar(): Promise<void> {
    const id = await this.calendarService.addWithDialog(this.booking());
    if (id) {
      this.booking.update(booking => ({ ...booking, calendarEventId: id }));
    }
  }

  async showInCalendar(): Promise<void> {
    await Calendar.openCalendar({ date: this.booking().start.getTime() });
  }
}
```

On iOS, the "Show in calendar" button appears after a save, because `addWithDialog(...)` returned the `id`. On Android it never appears from the dialog path, since the intent returns nothing; an app that wants the button there uses `addSilently(...)` behind a settings toggle such as "Sync bookings to my calendar" and asks for full access when the user switches it on, not at app start. Apple said at WWDC23 that it expects full access prompts to be declined more often than the lighter ones, and a prompt tied to a toggle the user flipped a moment ago is the one with a chance.

## Legacy tutorials

Most results for adding a calendar event in Ionic still describe `cordova-plugin-calendar` with the `@ionic-native/calendar` wrapper. Both have stopped moving. `cordova-plugin-calendar` published its last version, 5.1.6, on January 11, 2022, which predates the iOS 17 access levels of September 2023, and `@ionic-native/calendar` ended at 5.36.0 in August 2021 when Ionic Native became Awesome Cordova Plugins. Its successor wrapper, `@awesome-cordova-plugins/calendar`, still wraps the same Cordova plugin. A Capacitor app can load Cordova plugins, but a plugin that has not seen a release since 2022 does not know iOS 17's write-only and full access levels.

The Capacitor Calendar plugin covers the same ground with methods that map one to one:

| `cordova-plugin-calendar` | Capacitor Calendar plugin |
| --- | --- |
| `createEventInteractively(...)` | `displayCreateEvent(...)` |
| `createEvent(...)` / `createEventWithOptions(...)` | `createEvent(...)` with `alerts` and `recurrence` |
| `modifyEvent(...)` | `updateEventById(...)` |
| `deleteEvent(...)` | `deleteEventById(...)` |
| `findEvent(...)` / `listEventsInRange(...)` | `getEvents(...)` |
| `listCalendars()` | `getCalendars()` |
| `openCalendar(...)` | `openCalendar(...)` |
| `hasReadWritePermission()` / `requestReadWritePermission()` | `checkPermissions()` / `requestPermissions(...)` |

The Cordova plugin identified events for `modifyEvent(...)` by title and dates rather than by an identifier, which breaks as soon as the user renames the event. The Capacitor plugin returns an `id` from `createEvent(...)` and takes that `id` for every update and delete.

## FAQ

### What is the easiest way to add a calendar event in Capacitor without asking for full calendar access?

Call `displayCreateEvent(...)` of the [Capacitor Calendar plugin](../../sdks/capacitor/calendar.md) with the event data. The plugin opens the system event editor prefilled with your title, dates, location and description, and the user saves the event into a calendar of their choice. No permission prompt appears on Android or on iOS 17 and later, and the app needs no `Info.plist` key and no manifest entry for it.

### Do I need calendar permission to add an event in an Ionic app?

Not for the system dialog. `displayCreateEvent(...)` runs without any calendar permission on both platforms. `createEvent(...)`, which writes the event without a dialog, needs full access on iOS 17 and later and both `READ_CALENDAR` and `WRITE_CALENDAR` on Android, because the plugin looks up the target calendar before it writes.

### Can I tell whether the user saved the event in the dialog?

On iOS, yes: `displayCreateEvent(...)` resolves with the `id` of the saved event and without an `id` when the user canceled. On Android, the system dialog reports nothing back, so the promise resolves without an `id` either way. If your app must know, use `createEvent(...)`.

### Can I prefill a recurring event in the system dialog?

Yes. Pass a `recurrence` rule in the event data. On Android, the plugin writes it into the intent as an `RRULE`; on iOS, it is applied to the event the editor shows. [How to Manage Recurring Calendar Events in Capacitor](./recurring-calendar-events-in-capacitor.md) explains the rule format and how occurrences are edited later.

### Does the Capacitor Calendar plugin work with Ionic React or Vue?

Yes. The plugin is framework-agnostic and works in any Capacitor app. Only the service and page in this guide are Angular-specific; the plugin calls are the same in React, Vue or plain JavaScript. There is no web implementation, so the methods reject with an unimplemented error in the browser.

### Is the Capacitor Calendar plugin free?

No. It is part of the [Capawesome Insiders](../../insiders/index.md) subscription and installs from the Capawesome npm registry with your license key.

## Related posts

- [Capacitor Calendar Permissions on iOS 17+ and Android](./capacitor-calendar-permissions.md)
- [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)

## Try Capawesome Cloud

The calendar plugin and its `Info.plist` keys ship in a native build, and a permission flow is only testable on a device. Capawesome Cloud builds and signs your Ionic app for Android and iOS without a Mac, and [How to Sign & Build Capacitor Apps in the Cloud](./how-to-sign-and-build-your-capacitor-app-in-the-cloud.md) shows the setup.

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

## Conclusion

Start with the dialog. It needs no permission, no review justification and no code for the cases where the user changes their mind, and for a booking the user adds once it is all the calendar integration most apps need. Move a feature to `createEvent(...)` only when the app has to change the event later, put that feature behind a toggle, and request full access when the toggle is switched on. Before the permission dialog goes live, read [Capacitor Calendar Permissions on iOS 17+ and Android](./capacitor-calendar-permissions.md) for the iOS 17 states that look like bugs and are not.

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"}.
