ICS feeds

Read a subscribed iCalendar feed, such as a holiday calendar or a calendar shared by its secret address.

Sometimes all you have is a link: a public holiday calendar, a team's shared .ics address, the feed a booking tool publishes. This provider reads it like any other calendar, recurring series and all, and syncs it. It can't write, because a feed is a file someone else publishes.

npm install @cal-sdk/core @cal-sdk/ics
import { listEvents, syncEvents } from '@cal-sdk/core';
import { ics } from '@cal-sdk/ics';

const calendar = ics({ url: 'webcal://example.com/holidays.ics' });

const { events } = await listEvents({
  calendar,
  timeMin: '2026-11-01T00:00:00Z',
  timeMax: '2026-12-01T00:00:00Z',
});

// Later: only what changed since the last sync.
const { changes, nextCursor } = await syncEvents({ calendar, cursor });

Settings

urlstring
The feed address. webcal:// addresses are read over HTTPS.
timeZone?string= 'UTC'
Zone for times the feed gives without one, when it declares no X-WR-TIMEZONE.
headers?Record<string, string | undefined>
Headers sent with every request, such as authorization for a feed behind a password. Not sent when the feed redirects to another origin.
fetch?typeof fetch
A custom fetch, for example to log, cache or record requests.

Not supported

Feeds are read only, so creating, updating and deleting throw UnsupportedFunctionalityError. Search, reminders and colors aren't supported either. Free/busy is computed from events. See the comparison for every provider.

How it behaves

Downloading the feed

The feed is 'primary', the only calendar, and every call downloads it. There's no cache between calls, so if you list the same feed often, pass a fetch that caches. Private feed addresses hold a secret in their path or query, so errors only report the feed's origin.

Recurring events and time zones

calsdk expands recurring events itself, with their EXDATEs and their moved and cancelled instances. A series stops after 50,000 instances or a second of work, with a warning, so one runaway rule can't stall your app. Rules repeating every minute or second, with an INTERVAL above 1,000, or that RFC 5545 doesn't allow, list only their first instance. An instance in the hour skipped when clocks go forward is shown an hour later, the way Google, Apple and Outlook show it.

IANA names, Windows names and zones defined only in the feed are all understood. Times in UTC, without a zone, or in a zone defined only in the feed are shown in the calendar's zone. In a zone defined only in the feed, a time within the hour the clocks change may be off by that hour.

An event id is the feed's UID. Instances of a series are <UID>_<original start>, the start being a UTC date-time (20261103T100000Z) or, for all-day series, a date (20261103). Google Meet, Teams and Zoom links in the event's URL, location or description become its conference.

Sync

A feed has no change log, so calsdk compares each single event, series and moved instance with the last sync, and the cursor holds a short fingerprint per event. A cancelled instance is reported once as a delete, in the first sync too. When the server says the feed hasn't changed (ETag or Last-Modified), it isn't downloaded again.

Provider options

None. Events have no providerMetadata.

On this page