# Ical Calendar Feed Parsing (`first_watch/ical-calendar-feed-parsing`) Actor

Teams importing calendars from multiple providers need to validate, normalize, expand, deduplicate, and consolidate ICS data without silently losing events, shifting times, or overlooking scheduling conflicts.

- **URL**: https://apify.com/first_watch/ical-calendar-feed-parsing.md
- **Developed by:** [Jordan Nabbe](https://apify.com/first_watch) (community)
- **Categories:** Automation, Developer tools, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.30 / 1,000 normalized event instances

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ICS Batch Doctor - iCalendar Feed Parser, Validator and Normalizer

ICS Batch Doctor is for automation teams, calendar-integration developers, operations coordinators and SaaS vendors who have to import calendars exported from many different providers. Give it real .ics files (as text or base64), ZIP archives full of .ics files, or explicit HTTPS links. It gives back flat event-instance rows you can import right away: recurring meetings are expanded inside your date window, times are converted to UTC and to your target time zone, the original local values and TZID are kept for auditing, and every row names its source file. Validation findings, duplicates across calendars, scheduling conflicts and per-file failures go into a run summary, so one broken file never costs you the rows from the good ones.

### What it does

- **Parses the actual calendar bytes.** It handles RFC 5545 line unfolding, property parameters (quoted values included), text unescaping, nested components (alarms are skipped) and strict UTF-8 decoding. Base64 input is validated strictly.
- **Reads ZIP batches in memory.** Stored and DEFLATE entries are supported, and every entry is checked against its CRC. Path traversal, encrypted entries, ZIP64, split archives, unsupported compression and expansion ratios above 200:1 are rejected one entry at a time. The other entries keep processing.
- **Expands recurrence inside your window.** Supported: DAILY, WEEKLY, MONTHLY and YEARLY rules with INTERVAL, COUNT, UNTIL, BYDAY (including ordinals such as -1SU), BYMONTHDAY, BYMONTH and WKST. EXDATE, RDATE and RECURRENCE-ID overrides (moved or cancelled) are applied before duplicate and conflict analysis. When a month has no such date (for example the 31st), that month is skipped rather than rolled over.
- **Handles time zones honestly.** IANA zones come from Node's built-in time-zone database. Embedded VTIMEZONE STANDARD/DAYLIGHT rules are interpreted. An unknown TZID produces an error finding and is never guessed. iCalendar dates use the fixed YYYYMMDD form, so there is no day/month ambiguity to resolve.
- **Stores one clean row per event instance** in the dataset. Each row has stable eventId and canonicalId, UID, summary, description, location, allDay, original start/end and TZID, startUtc/endUtc, startTarget/endTarget with UTC offset, recurrence identity, RRULE, status, transparency, organizer, attendees (as a JSON string), URL, created/modified timestamps and sequence.
- **Writes a run summary to the OUTPUT record.** It holds the outcome block (status, items received/succeeded/failed, billable count, limit flag, undelivered rows), the delivered rows, and issues with severity, code, property, line number and suggested repair. It also lists duplicate groups, conflicts and per-source failures with the stage where each failed. Duplicate groups and conflicts reference emitted event IDs, so you can join them without parsing messages.

### How to use it

1. Click **Start** with the prefilled input. It runs fully offline on an inline sample calendar.
2. Or paste your own input:

```json
{
  "sources": [
    {
      "sourceId": "team",
      "fileName": "team.ics",
      "mediaType": "text/calendar",
      "encoding": "utf8",
      "content": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:planning@example.com\r\nDTSTART:20261005T090000Z\r\nDTEND:20261005T100000Z\r\nRRULE:FREQ=WEEKLY;COUNT=2\r\nSUMMARY:Operations planning\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
    }
  ],
  "dateWindow": { "start": "2026-10-01T00:00:00Z", "end": "2026-11-01T00:00:00Z" },
  "targetTimeZone": "Europe/London",
  "deduplication": { "enabled": true, "mode": "uid_or_fingerprint" },
  "conflictDetection": { "enabled": true, "includeTransparentEvents": false },
  "limits": { "maxInstances": 1000, "maxSourceBytes": 10485760 }
}
```

For original file bytes, set `encoding` to `base64`. For ZIP archives, also use `mediaType: application/zip` and a `.zip` file name. Source IDs must be unique. The window includes its start and excludes its end. Events are selected when they overlap the window and are never clipped.

### Output example

| sourceRef | summary | allDay | startUtc | startTarget | recurring |
|---|---|---|---|---|---|
| operations-calendar:operations.ics | Maintenance review | false | 2026-10-05T09:00:00Z | 2026-10-05T10:00:00+01:00 | true |
| operations-calendar:operations.ics | Maintenance review | false | 2026-10-12T09:00:00Z | 2026-10-12T10:00:00+01:00 | true |
| operations-calendar:operations.ics | Team offsite | true | 2026-10-19T23:00:00Z | 2026-10-20T00:00:00+01:00 | false |

`OUTPUT.outcome.status` takes one of these values:

- `SUCCEEDED`
- `PARTIAL`: some sources or events failed.
- `LIMIT_REACHED`: your spending limit or your `maxInstances` cap stopped delivery. `undelivered` counts the occurrences withheld, and an `INSTANCE_LIMIT` issue reports `skippedInstances` when the cap was the cause.
- `FAILED`

If no usable event row was produced, the summary is still written, and then the run exits with an error so a failure is never reported as a success. Invalid input is rejected before any processing.

### Pricing

Pay per event. The main charge is per event instance; everything else listed below is free.

| Event | Price (USD) | Charged when |
|---|---|---|
| Normalized event instance (`apify-default-dataset-item`) | $0.0003 | A normalized event instance row is stored in the default dataset. |

Not charged:

- issue findings
- duplicate groups
- conflicts
- source failures
- OUTPUT summary

Examples (default 512 MB memory, one start event):

- One weekly meeting expanded to two instances: 2 × $0.0003 = **$0.0006**
- Batch of 1000 event instances: 1000 × $0.0003 = **$0.30**

Spending limit: when you set a maximum cost per run, the Actor stops before it would exceed it, keeps every event instance it already delivered and reports `limitReached` in the `OUTPUT` record. It never delivers results beyond the limit.

The Store's Pricing tab shows the prices in force. If this section and the Pricing tab ever differ, the Pricing tab applies.

### Use cases

- Validate batches of calendar exports before a migration or bulk import.
- Build UTC event tables for reporting, BI or synchronization jobs.
- Audit time-zone conversions while keeping the original local time and TZID.
- Find the same meeting imported from several calendars.
- Flag overlapping events for operations planning.
- Feed structured calendar rows and machine-readable findings into databases, spreadsheets or LLM pipelines.

### Limits

- **Scope.** This is a bounded calendar importer, not a complete RFC 5545 scheduling engine.
- **Dates and windows.** Dates must fall between 1900 and 2100. Windows can be up to 3660 days.
- **Run size.** Each run accepts up to 100 sources, up to 1000 calendar documents and up to 1000 entries per archive. It emits at most `maxInstances` event instances (up to 10000). Occurrences beyond the cap are counted, not emitted, and the run reports `LIMIT_REACHED`.
- **Data size.** Each source defaults to 10 MiB (maximum 25 MiB). Total decoded data is capped at 32 MiB.
- **Time and work.** Processing has a four-minute deadline and a work budget. Very long recurrence histories may need a narrower window.
- **Reporting.** Hitting any limit produces an explicit finding, source failure or outcome status.

Not supported:

- Sub-daily frequencies, BYSETPOS, BYWEEKNO, BYYEARDAY and BYHOUR/BYMINUTE/BYSECOND.
- Period-valued RDATE and RECURRENCE-ID RANGE.
- Alarms, attachments and free/busy data.

If a file repeats the master event for the same UID, that UID is reported as an issue. The Actor does not silently pick one revision.

Remote sources are limited to the exact HTTPS hosts `apify.com`, `github.com` and `stackoverflow.com`. Redirects to any other host are rejected, and that includes GitHub's raw-content host. For Google, Microsoft, iCloud, Proton or private feeds, download the file and pass it inline. Downloads resolve to public IPv4 addresses only, check every redirect (at most five), require a calendar, ZIP or plain-text content type and time out after 30 seconds. Links inside calendars are never fetched.

### FAQ

**Does it really parse files, or only pre-extracted text?**
It parses real ICS bytes and reads ZIP archives through their central directory, inflating entries with Node zlib.

**Does the sample need internet access?**
No. The prefilled input is inline. No network request is made for inline sources.

**Will one invalid file stop the run?**
No. A bad event produces an issue, and a bad source or archive entry produces a source failure. All other sources continue.

**What happens when my calendar has more occurrences than maxInstances?**
The first occurrences up to the cap are delivered, the rest are counted in `undelivered`, and the status is `LIMIT_REACHED`. Raise the cap or narrow the window to get the rest.

**Are duplicates removed?**
No. Duplicate groups identify matching occurrences across different sources. Every event row is kept, so you decide which one wins.

**Does it produce quality or confidence scores?**
No. It reports only measured facts: counts, codes, line numbers and properties.

**Is my calendar content private?**
Calendar content is never written to logs and no temporary files are created. Event fields do appear in your results, so keep the run's storage private when you process confidential calendars. Remote URLs are not copied into results.

# Actor input Schema

## `sources` (type: `array`):

1-100 sources with unique sourceId values. Inline source: {sourceId, fileName (.ics or .zip), mediaType (text/calendar or application/zip), encoding (utf8 or base64; ZIP requires base64), content}. Remote source: {sourceId, url, optional fileName}. Inline sources are processed without any network request.

## `dateWindow` (type: `object`):

Object {start, end} in YYYY-MM-DDTHH:mm:ssZ (UTC). Start is inclusive, end is exclusive, at most 3660 days, years 1900-2100. Recurring events are expanded only inside this window and selected by interval overlap.

## `targetTimeZone` (type: `string`):

IANA time zone (for example Europe/London or America/New_York) used for startTarget/endTarget, floating local times and all-day dates. UTC times and original values are always kept.

## `deduplication` (type: `object`):

Object {enabled: true|false, mode: uid | uid_or_fingerprint}. Groups matching occurrences across different sources (same UID and occurrence, or optionally same title, location, start and end). All event rows are kept; groups are listed in the OUTPUT record.

## `conflictDetection` (type: `object`):

Object {enabled: true|false, includeTransparentEvents: true|false}. Reports pairwise time overlaps between emitted events in the OUTPUT record. It does not infer shared attendees or rooms.

## `limits` (type: `object`):

Object {maxInstances: 1-10000, maxSourceBytes: 1024-26214400}. maxInstances caps emitted event rows per run; when the cap is hit the run reports LIMIT_REACHED and counts the withheld occurrences. maxSourceBytes caps each decoded or downloaded source and each extracted ZIP entry. Hitting a limit is reported explicitly, never silently.

## Actor input object example

```json
{
  "sources": [
    {
      "sourceId": "operations-calendar",
      "fileName": "operations.ics",
      "mediaType": "text/calendar",
      "encoding": "utf8",
      "content": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Example Corp//Operations//EN\r\nBEGIN:VEVENT\r\nUID:maintenance-42@example.com\r\nDTSTAMP:20260901T090000Z\r\nDTSTART:20261005T090000Z\r\nDTEND:20261005T100000Z\r\nRRULE:FREQ=WEEKLY;COUNT=2\r\nSUMMARY:Maintenance review\r\nLOCATION:Operations room\r\nSTATUS:CONFIRMED\r\nSEQUENCE:1\r\nORGANIZER;CN=Ops Lead:mailto:ops-lead@example.com\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:offsite-7@example.com\r\nDTSTAMP:20260901T090000Z\r\nDTSTART;VALUE=DATE:20261020\r\nDTEND;VALUE=DATE:20261021\r\nSUMMARY:Team offsite\r\nDESCRIPTION:Quarterly planning - bring your badge\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
    }
  ],
  "dateWindow": {
    "start": "2026-10-01T00:00:00Z",
    "end": "2026-11-01T00:00:00Z"
  },
  "targetTimeZone": "Europe/London",
  "deduplication": {
    "enabled": true,
    "mode": "uid_or_fingerprint"
  },
  "conflictDetection": {
    "enabled": true,
    "includeTransparentEvents": false
  },
  "limits": {
    "maxInstances": 100,
    "maxSourceBytes": 10485760
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Every result row stored in the default dataset.

## `summary` (type: `string`):

Summary written to the OUTPUT record.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "sources": [
        {
            "sourceId": "operations-calendar",
            "fileName": "operations.ics",
            "mediaType": "text/calendar",
            "encoding": "utf8",
            "content": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Example Corp//Operations//EN\r\nBEGIN:VEVENT\r\nUID:maintenance-42@example.com\r\nDTSTAMP:20260901T090000Z\r\nDTSTART:20261005T090000Z\r\nDTEND:20261005T100000Z\r\nRRULE:FREQ=WEEKLY;COUNT=2\r\nSUMMARY:Maintenance review\r\nLOCATION:Operations room\r\nSTATUS:CONFIRMED\r\nSEQUENCE:1\r\nORGANIZER;CN=Ops Lead:mailto:ops-lead@example.com\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:offsite-7@example.com\r\nDTSTAMP:20260901T090000Z\r\nDTSTART;VALUE=DATE:20261020\r\nDTEND;VALUE=DATE:20261021\r\nSUMMARY:Team offsite\r\nDESCRIPTION:Quarterly planning - bring your badge\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
        }
    ],
    "dateWindow": {
        "start": "2026-10-01T00:00:00Z",
        "end": "2026-11-01T00:00:00Z"
    },
    "targetTimeZone": "Europe/London",
    "deduplication": {
        "enabled": true,
        "mode": "uid_or_fingerprint"
    },
    "conflictDetection": {
        "enabled": true,
        "includeTransparentEvents": false
    },
    "limits": {
        "maxInstances": 100,
        "maxSourceBytes": 10485760
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("first_watch/ical-calendar-feed-parsing").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "sources": [{
            "sourceId": "operations-calendar",
            "fileName": "operations.ics",
            "mediaType": "text/calendar",
            "encoding": "utf8",
            "content": """BEGIN:VCALENDAR\r
VERSION:2.0\r
PRODID:-//Example Corp//Operations//EN\r
BEGIN:VEVENT\r
UID:maintenance-42@example.com\r
DTSTAMP:20260901T090000Z\r
DTSTART:20261005T090000Z\r
DTEND:20261005T100000Z\r
RRULE:FREQ=WEEKLY;COUNT=2\r
SUMMARY:Maintenance review\r
LOCATION:Operations room\r
STATUS:CONFIRMED\r
SEQUENCE:1\r
ORGANIZER;CN=Ops Lead:mailto:ops-lead@example.com\r
END:VEVENT\r
BEGIN:VEVENT\r
UID:offsite-7@example.com\r
DTSTAMP:20260901T090000Z\r
DTSTART;VALUE=DATE:20261020\r
DTEND;VALUE=DATE:20261021\r
SUMMARY:Team offsite\r
DESCRIPTION:Quarterly planning - bring your badge\r
TRANSP:TRANSPARENT\r
END:VEVENT\r
END:VCALENDAR\r
""",
        }],
    "dateWindow": {
        "start": "2026-10-01T00:00:00Z",
        "end": "2026-11-01T00:00:00Z",
    },
    "targetTimeZone": "Europe/London",
    "deduplication": {
        "enabled": True,
        "mode": "uid_or_fingerprint",
    },
    "conflictDetection": {
        "enabled": True,
        "includeTransparentEvents": False,
    },
    "limits": {
        "maxInstances": 100,
        "maxSourceBytes": 10485760,
    },
}

# Run the Actor and wait for it to finish
run = client.actor("first_watch/ical-calendar-feed-parsing").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sources": [
    {
      "sourceId": "operations-calendar",
      "fileName": "operations.ics",
      "mediaType": "text/calendar",
      "encoding": "utf8",
      "content": "BEGIN:VCALENDAR\\r\\nVERSION:2.0\\r\\nPRODID:-//Example Corp//Operations//EN\\r\\nBEGIN:VEVENT\\r\\nUID:maintenance-42@example.com\\r\\nDTSTAMP:20260901T090000Z\\r\\nDTSTART:20261005T090000Z\\r\\nDTEND:20261005T100000Z\\r\\nRRULE:FREQ=WEEKLY;COUNT=2\\r\\nSUMMARY:Maintenance review\\r\\nLOCATION:Operations room\\r\\nSTATUS:CONFIRMED\\r\\nSEQUENCE:1\\r\\nORGANIZER;CN=Ops Lead:mailto:ops-lead@example.com\\r\\nEND:VEVENT\\r\\nBEGIN:VEVENT\\r\\nUID:offsite-7@example.com\\r\\nDTSTAMP:20260901T090000Z\\r\\nDTSTART;VALUE=DATE:20261020\\r\\nDTEND;VALUE=DATE:20261021\\r\\nSUMMARY:Team offsite\\r\\nDESCRIPTION:Quarterly planning - bring your badge\\r\\nTRANSP:TRANSPARENT\\r\\nEND:VEVENT\\r\\nEND:VCALENDAR\\r\\n"
    }
  ],
  "dateWindow": {
    "start": "2026-10-01T00:00:00Z",
    "end": "2026-11-01T00:00:00Z"
  },
  "targetTimeZone": "Europe/London",
  "deduplication": {
    "enabled": true,
    "mode": "uid_or_fingerprint"
  },
  "conflictDetection": {
    "enabled": true,
    "includeTransparentEvents": false
  },
  "limits": {
    "maxInstances": 100,
    "maxSourceBytes": 10485760
  }
}' |
apify call first_watch/ical-calendar-feed-parsing --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,first_watch/ical-calendar-feed-parsing"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/67wTbsfAneGrXohpV/builds/0rbeS4Olii3LO5BVM/openapi.json
