# Luma Events Scraper - Public Calendars & Event Changes (`maydit/luma-events-scraper`) Actor

Export public Luma city and organizer calendar subscriptions. Stable event IDs, dates, locations, literal topic evidence and saved schedule changes. No login or private guest data.

- **URL**: https://apify.com/maydit/luma-events-scraper.md
- **Developed by:** [Brandt May](https://apify.com/maydit) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 event observations

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Luma Events Scraper - Public Calendars & Event Changes

![Public Luma calendars to Events & schedule changes - illustrative workflow](https://mayd-it.com/assets/actors/luma-events-scraper.png)

*Mayd It data tools | Public calendars*

For event discovery and calendar monitoring: turn public Luma city and organizer calendar subscriptions into event rows with stable IDs, dates and locations. This reads public calendar exports, without guest lists or private attendee data.

### Start here

- **First run:** Use **San Francisco Public Luma Events** below, with 10 saved results and a 100-event calendar window. Inspect the first output before enabling saved comparisons.
- **Read the results:** Open the default Dataset for events and **SUMMARY** for calendar coverage. The dates, source event link and changed fields let you inspect a saved schedule change.
- **Keep in mind:** Exports can include past events and are bounded by the selected window. Keyword matching is literal; a missing event does not prove cancellation or deletion.

Turn Luma's public city and organizer calendar subscriptions into event records with stable IDs, normalized times, public locations and source links. Add literal topic filters or compare saved observations to detect changes in schedules, titles, status, location and exported descriptions.

Useful for a city event watchlist, an organizer calendar export, or a recurring event research workflow. No API key, login, proxy or browser is required.

### Quick start

Run with `{}` to save up to 20 events visible in the San Francisco public calendar feed. The Console prefill uses the same source. Events are ordered by the exported start date ascending; organizer feeds can include historical events. Use `startDate` when you only want dates on or after a specific day.

```json
{
  "calendarUrls": ["https://luma.com/sf"],
  "keywords": ["AI", "founder"],
  "maxResults": 20,
  "maxEventsPerCalendar": 100
}
```

Each saved row is one unique exported event. Pricing is **$0.003 per saved event ($3 per 1,000)**. Duplicates, filtered events, unchanged rows skipped by `onlyChanges`, and error diagnostics are not saved as paid results. Spending limits can stop a run before its requested window is finished.

### Supported sources and coverage

- Public city pages such as `https://luma.com/sf` and `https://luma.com/nyc`.
- Public organizer calendar pages such as `https://luma.com/ignitegtm`.
- Public subscriptions in the form `https://api.lu.ma/ics/get?entity=calendar&id=cal-...` or `entity=discover&id=discplace-...`.

The Actor resolves the public page's calendar identifier and reads the supported iCalendar export described in [Luma's subscription help](https://help.luma.com/p/ical-syncing). A general one-segment event page can be syntactically accepted as an input URL, but will fail with a clear unsupported-page error when it is not a calendar/city. Private calendars, personal `entity=user` feeds, tokens and event detail paths are unsupported. Source robots rules and public network restrictions apply to every request and redirect.

This is the **currently visible subscription window**, not all events or complete history. During source verification, a city page reported 70 events while its subscription exported 50. The source may retain past events or omit events. Disappearance is never declared a cancellation or deletion. `SUMMARY.sourceCoverage` reports exported, selected, rejected and saved counts, source refresh hints and any page-reported count.

The export publishes a **12-hour refresh interval**. Schedule no more frequently than twice daily unless the publisher changes its interval. A repeated fetch does not promise fresher underlying content.

### Inputs

| Input | Behavior |
| --- | --- |
| `calendarUrls` | Up to 10 public city/calendar URLs. Omitted or empty uses San Francisco. Duplicate sources are removed and processing order is sorted. |
| `keywords` | Up to 20 case-insensitive literal phrases, at most 100 characters each. Any phrase may match title, location or the first 10,000 description characters. |
| `startDate`, `endDate` | Optional real `YYYY-MM-DD` dates, inclusive, filtering the event's exported DTSTART date. UTC exports therefore use the UTC calendar date, not an assumed venue date. |
| `maxEventsPerCalendar` | Default 100, range 1-500. Select the earliest visible usable events before date/topic filtering. Increase this window if an organizer feed contains many older events. |
| `maxResults` | Default 20, maximum 1,000 unique saved rows across all selected sources. |
| `comparePrevious` | Return all matching observations with their comparison status and changed fields. Default false. |
| `onlyChanges` | Implies comparison and skips unchanged observations. Default false. |
| `snapshotName` | History namespace, default `default`; 1-80 letters, digits, underscores or hyphens. |
| `maxRunSeconds` | Default 240; range 30-3,600. Also respects the platform deadline minus 25 seconds. |

### Output and date semantics

| Fields | Meaning |
| --- | --- |
| `eventId`, `uid`, `recurrenceId` | SHA-256 identity derived from the feed UID and any explicit recurrence instance. The source UID is also retained. Duplicate identities across calendars save once; the first source's metadata is retained. |
| `title`, `eventUrl` | Exported title and public Luma URL if published as a URL property or inside the description. Missing links are null. |
| `sourceUrl`, `feedUrl`, `calendarId`, `calendarName` | Provenance of the exported observation. |
| `startDate`, `endDate`, `startLocal`, `endLocal` | Dates and date-time strings in the feed's own representation. `startLocal` may include `Z` when the feed supplied UTC. |
| `startsAt`, `endsAt`, `timeZone`, `endTimeZone` | ISO UTC times when the export supplies UTC or a resolvable embedded timezone. Start and end timezone labels are retained separately; floating/unresolved times have UTC fields null. `calendarTimeZone` is separate page metadata and never silently substitutes an offset. |
| `allDay`, `endDateExclusive`, `timeUnresolved`, `endTimeUnresolved` | All-day dates are not fabricated midnight UTC timestamps. An all-day DTEND is exclusive. `timeUnresolved` flags an unresolved timed start or end; `endTimeUnresolved` identifies the end specifically. |
| `status` | Calendar status, such as `TENTATIVE` or `CANCELLED`. This is not ticket availability or registration approval. |
| `location` | Only the exported location string. It can be a URL or an instruction to check the event page. Private locations are not enriched. |
| `description`, `descriptionHash` | Publisher's exported text, capped at 10,000 characters. The hash covers the full received description. |
| `descriptionTruncated`, `publisherAbbreviationDetected` | The first flag identifies this Actor's 10,000-character cutoff. The second is a visible ellipsis heuristic, not proof that the original description is complete or incomplete. Publishers often abbreviate export text. |
| `matchedTerms` | Literal matching phrase, field and short excerpt. Empty when no topic filter was requested. |
| `comparisonStatus`, `changedFields`, `previous`, `previousObservedAt` | Saved-history comparison, as described below. |

Recurring rules requiring recurrence expansion are skipped and reported as partial coverage; explicit recurrence instances can be identified. Invalid dates, broken intervals and malformed records also appear in `SUMMARY`, never as event rows. No ticket prices, attendee lists, guest counts or private contact details are promised or extracted.

### Repeated runs and reliable history

Enable `comparePrevious` to receive `new`, `changed` or `unchanged`; otherwise status is `not_requested`. `onlyChanges` saves new/changed observations only. The first comparison run warns that it is establishing observations. A first row marked `new` describes the comparison result; **`SUMMARY.historyUpdated` confirms whether the history write succeeded**.

History uses the named store `maydit-luma-public-calendar-history`, scoped by `snapshotName`, sorted source URLs, keywords and date filters. Only confirmed saved rows advance it. Other observations remain intact on partial runs, limits and filtered windows. No output row means no new baseline for that identity. Volatile `DTSTAMP`, `SEQUENCE` and observation time do not trigger changes.

Use **one concurrent writer per history scope**. A reread detects some concurrent changes, but it is not a transactional lock. Malformed/incompatible history fails clearly; a new `snapshotName` starts fresh. History is bounded at 20,000 identities per scope.

### Run status and limitations

`SUMMARY` in the default key-value store records failures, rejected entries, window limits, unchanged/duplicate counts and stops caused by time or spending limits. A requested result/window cap is a successful bounded run; it is not complete historical coverage. Source failures produce `partial` when useful observations survived. If every source fails and nothing usable is saved, the run fails after writing its summary. Empty legitimate feeds or unmatched filters can succeed with a warning and zero rows.

Requests have a 25-second limit and bounded retries for transient failures. There is no login or challenge bypass. Exported source text and locations can be incomplete or stale; verify event details at the source before attending or making decisions. Public export access does not grant blanket republication rights.

### Ready-to-run examples

The following exact inputs also appear in `examples.json`.

#### San Francisco Public Luma Events

Export 10 events visible in the San Francisco public Luma calendar feed, with source links and normalized times.

```json
{
  "calendarUrls": [
    "https://luma.com/sf"
  ],
  "maxResults": 10,
  "maxEventsPerCalendar": 100
}
```

#### New York Public Luma Calendar Events

Read 10 visible New York events from Luma's documented public calendar subscription, without private guest or ticket data.

```json
{
  "calendarUrls": [
    "https://luma.com/nyc"
  ],
  "maxResults": 10,
  "maxEventsPerCalendar": 100
}
```

#### Ignite Community Luma Calendar - Change Watch

Compare 10 visible Ignite Community calendar events with previously saved observations. The first run establishes a baseline; the export can contain historical events.

```json
{
  "calendarUrls": [
    "https://luma.com/ignitegtm"
  ],
  "maxResults": 10,
  "maxEventsPerCalendar": 100,
  "comparePrevious": true,
  "snapshotName": "ignite-community-demo"
}
```

# Actor input Schema

## `calendarUrls` (type: `array`):

Up to 10 public Luma city/calendar page URLs, or their public calendar/discover iCal URLs. Empty/omitted uses San Francisco. Personal/user feeds, tokens, login-only calendars and event detail URLs are rejected. Exports are bounded; they do not promise every event.

## `keywords` (type: `array`):

Optional up to 20 literal case-insensitive phrases, each at most 100 characters. Match any phrase in title, location or the first 10,000 exported description characters. Matching excerpts are returned. This is not semantic intent inference.

## `startDate` (type: `string`):

Optional real YYYY-MM-DD date, inclusive. Compared with the DTSTART date as written in the feed; UTC exports use the UTC date. Calendar timezone metadata is separate.

## `endDate` (type: `string`):

Optional real YYYY-MM-DD date, inclusive, in the same feed date convention as startDate.

## `maxEventsPerCalendar` (type: `integer`):

Inspect this many usable events per current export, sorted by exported start date ascending, then stable ID. This may include historical events in organizer calendars. Filter afterward. No full-history or disappearance claims.

## `maxResults` (type: `integer`):

Maximum unique matching rows saved across the whole run. Normal requested caps are bounded coverage, not source failures.

## `comparePrevious` (type: `boolean`):

Return matching rows with new, changed or unchanged comparison status. Uses a named persistent store. Only confirmed saved rows advance history. The first run establishes observations, and SUMMARY confirms whether history was committed.

## `onlyChanges` (type: `boolean`):

Implies comparison. Unchanged matching observations are skipped without a dataset charge. A first run saves matching observations; subsequent unchanged runs can correctly save zero. No disappearance/deletion inference.

## `snapshotName` (type: `string`):

Use 1-80 letters, digits, underscores or hyphens. History also includes the sorted sources/queries and content filters. Run only one writer at a time for each scope. Start a new name for an incompatible saved baseline.

## `maxRunSeconds` (type: `integer`):

Requests use this budget and the platform deadline minus 25 seconds. Each request is capped at 25 seconds. Partial coverage and spending stops are explicit in SUMMARY.

## Actor input object example

```json
{
  "calendarUrls": [
    "https://luma.com/sf"
  ],
  "maxEventsPerCalendar": 100,
  "maxResults": 20,
  "comparePrevious": false,
  "onlyChanges": false,
  "snapshotName": "default",
  "maxRunSeconds": 240
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

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

No description

# 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 = {
    "calendarUrls": [
        "https://luma.com/sf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("maydit/luma-events-scraper").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 = { "calendarUrls": ["https://luma.com/sf"] }

# Run the Actor and wait for it to finish
run = client.actor("maydit/luma-events-scraper").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 '{
  "calendarUrls": [
    "https://luma.com/sf"
  ]
}' |
apify call maydit/luma-events-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,maydit/luma-events-scraper"
        }
    }
}
```

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/ldKEVlJJ5PiJLhzbJ/builds/LIBkd1eX9xdvz1AZX/openapi.json
