# ICS feeds (https://www.cal-sdk.com/docs/providers/ics)

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
npm install @cal-sdk/core @cal-sdk/ics
```

```ts
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 [#settings]

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

## Not supported [#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](/docs/providers) for every provider.

## How it behaves [#how-it-behaves]

### Downloading the feed [#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 [#recurring-events-and-time-zones]

calsdk expands recurring events itself, with their `EXDATE`s 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.

### Ids and video links [#ids-and-video-links]

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 [#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 [#provider-options]

None. Events have no `providerMetadata`.
