# Instagram Event Extractor — Posters, Dates & Changes (`renaissant_vibraphone/instagram-event-extractor`) Actor

Turn public Instagram captions and event poster images into structured event records. Track new and changed events. Bring your own Gemini API key.

- **URL**: https://apify.com/renaissant_vibraphone/instagram-event-extractor.md
- **Developed by:** [AmirHossein Naseri](https://apify.com/renaissant_vibraphone) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 post analyzeds

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

## Instagram Event Extractor

Turn public Instagram captions and event poster images into structured event records. Export JSON or CSV, and monitor new or changed announcements across runs.

**Version 0.1.0: a deployable first version.** Automated tests use mocked services. The offline demo uses clearly marked synthetic events. A live Instagram + Gemini run must be verified with your own credentials before publishing a paid listing.

### Start here

Read **[DEPLOYMENT.md](DEPLOYMENT.md)** for the complete beginner-friendly setup, deployment, first live test, scheduling, monetization and troubleshooting guide. **[STORE-LISTING.md](STORE-LISTING.md)** contains copy for your Store listing.

Requirements: Node.js 22+ (a current Node.js 24 LTS installation is recommended), npm, an Apify account, and a Google Gemini API key for live extraction. Customers bring their own Gemini API keys. You do not need a web server or a database subscription.

```bash
npm ci
npm test
npm run check
npm run demo
```

The demo makes no external calls. It writes `demo-output/events.json`, `events.csv`, `summary.json` and `second-run-summary.json`. The first simulated run returns two events; the second uses the cache and returns zero changes. This tests plumbing and history, not AI recognition accuracy.

### Deploy

From this project directory:

```bash
npx apify-cli@1.10.0 login
npx apify-cli@1.10.0 push --open
```

The CLI deploys the name in `.actor/actor.json`. Keep it different from your menu consistency checker. Check the name in your account before pushing; an existing Actor with the same name can be updated. You can use a different new name in the manifest. Deployment creates a private Actor; Store publication is a separate step.

### Run modes

#### 1. Demo

```json
{ "source": "demo", "monitorName": "my-first-demo" }
```

Synthetic fixtures only. No Instagram collection or Gemini analysis. Cloud platform compute may still cost money. To keep this Actor's own demo fee at zero, configure only the custom `post-analyzed` monetization event; do not add automatic start or dataset-item fees.

#### 2. Instagram profiles

```json
{
  "source": "instagram",
  "usernames": ["replace_with_a_public_venue"],
  "defaultTimezone": "Europe/Rome",
  "maxPostsPerProfile": 3,
  "maxTotalPosts": 3,
  "maxImagesPerPost": 2,
  "scraperBudgetCents": 50,
  "monitorName": "my-first-live-test",
  "onlyChanges": true
}
```

Replace the username. Enter your key in the secret `geminiApiKey` form field. Alternatively set `GEMINI_API_KEY` in your own private Actor environment. Do not embed an owner-funded key in a published Actor's shared configuration.

The Actor calls `apify/instagram-post-scraper` using its verified `username` and `resultsLimit` fields. It explicitly requests basic data and disables the music add-on. When several profiles are supplied, the per-profile limit is reduced if necessary to stay within `maxTotalPosts`. Upstream results may contain videos or errors, which count toward the inspected-row limit but are skipped by extraction.

#### 3. Existing posts dataset

Choose **Existing Apify dataset** in the form and select a dataset produced by the Instagram Post Scraper. This avoids starting another scraper. The same Gemini key requirement applies. The dataset picker grants read access under Apify's limited-permissions model.

Input records are expected to contain `id`, `caption`, `timestamp`, `url`, `ownerUsername`, `type`, `displayUrl`, `images` and/or `childPosts`. Not every field is mandatory; an ID and a usable caption or image are necessary. The collector reads a bounded prefix of the dataset, rather than scanning unlimited rows to find supported posts. Refresh the source dataset yourself when monitoring it; this Actor does not refresh an existing dataset.

### Outputs

The default dataset contains one row per event occurrence. `SUMMARY` in the run's key-value store explains counts, skipped posts, extraction failures, upstream run ID, budget stops and history location. `EVENTS.csv` is a spreadsheet-safe CSV. Each successful fresh analysis is also accessible as an `ANALYSIS-<hash>` record in the run's key-value store, including analyses that found no events.

| Field | Meaning |
|---|---|
| `eventId` | Stable identity within this monitor's retained history |
| `title` | Event name supported by the source |
| `startDate`, `endDate` | Local calendar dates, `YYYY-MM-DD`, or null |
| `startTime`, `endTime`, `timezone` | Local 24-hour times and IANA timezone; no silent UTC conversion |
| `venue`, `city`, `address`, `country` | Source-backed location details, or null |
| `ticketPrice`, `currency`, `ticketUrl` | Numeric stated price, currency code and explicit link, or null |
| `status` | `scheduled`, `cancelled` or `postponed` |
| `confidence`, `needsReview`, `reviewReasons` | Extraction confidence and reasons to inspect the source |
| `evidence` | Quoted words supporting the extraction |
| `sourcePost`, `sourcePostId`, `sourceUsername`, `sourcePublishedAt` | Provenance |
| `change`, `changedFields` | `new`, `changed`, or `unchanged`, with the fields that changed |
| `firstSeenAt`, `lastSeenAt` | Observation timestamps |
| `demo` | True for synthetic data |

For a sample, run `npm run demo` and inspect the generated JSON.

### Dates, duplicates and history

- Dates without an explicit year remain uncertain. Relative dates can be resolved against the post's publication timestamp. The model is instructed never to infer a year from the current wall clock.
- Missing or questionable details remain null or flagged; AI can still make mistakes. Validate a representative set of real posters before relying on it.
- Set the venue's timezone. A default timezone is an assumption and is flagged when the source does not supply one.
- Exact title/date/location matches can be merged across posts. Separate recurring dates remain separate. Same-post date/time edits can preserve event identity when unambiguous. A rescheduling announcement in a different post is not guaranteed to be matched to the original event.
- An absent or deleted post never automatically cancels an event. Cancellations need explicit source evidence.
- A named key-value store keeps extraction cache and event history. Its name includes this Actor's ID and your `monitorName`. State keys isolate profiles/dataset, model, timezone and extraction limits. Reuse the same configuration for repeated monitoring.
- Successful unchanged posts skip Gemini and this Actor's analysis fee. Instagram mode still refreshes its upstream posts and can incur scraper fees. Signed CDN URL query changes alone do not invalidate the cache.
- `onlyChanges: false` returns all eligible events in the current input window, not every event ever observed. `forceReprocess: true` analyzes unchanged posts again and incurs normal costs.
- History retains records seen within 90 days and at most 10,000 posts and 10,000 events. A retained event is not a permanent calendar archive.
- Run only one job at a time per monitor. There is no distributed lock; overlapping jobs can overwrite state. Output delivery is at least once after a crash between dataset write and history save. Downstream consumers should upsert by `eventId`.

### Cost controls and monetization

**There are separate cost components:** this Actor's fee (if you configure one), upstream scraper charges, Gemini usage on the supplied API key, and applicable Apify platform usage. The post-analysis price is not an all-inclusive total.

`scraperBudgetCents` caps the upstream PPE run only. The platform's maximum charge on this Actor governs its own event fees. Neither limit caps Gemini. Post/image limits bound work; monitor your Gemini quotas and billing separately. No exact total-cost estimate is promised without measuring actual inputs.

The code is ready for one custom pay-per-event event: **`post-analyzed`**. It is charged once per successful complete fresh or forced post analysis, even when the result is zero events. A failed or visually incomplete analysis, cached result or demo fixture does not emit that event. Price configuration and publishing must be done in Apify Console. Do not combine it with automatic dataset-item fees, which would charge again for multi-event posts.

### Scope and limitations

This release handles public image posts, captions, and image slides in carousels. It skips whole video/Reel posts and video slides. It does not analyze Stories, audio, linked pages, ticket inventory or whole cities automatically. Ticket links are extracted only when written explicitly in the analyzed source; "link in bio" is not resolved.

CDN downloads are limited to Instagram/Facebook image domains, JPEG/PNG/WebP/GIF, 8 MiB per image, and 16 MiB total per post. Redirects and arbitrary image hosts are rejected. An analysis with failed or rejected image downloads is stored only in its audit record with `complete: false`; it does not update the event feed, enter the successful cache or incur this Actor's analysis fee. It is retried on the next run, so upstream, Gemini and platform costs can recur. Configured caption/carousel truncation instead remains a complete, cacheable analysis with review warnings. Only the configured number of images and events per post can be processed. Posters with many occurrences or unreadable typography need review.

Extraction sends captions and selected image bytes to Google's Gemini API. No Instagram passwords, login cookies or private content are requested. Use data you are authorized to access and check applicable platform terms and data-handling requirements. This tool is independent of Instagram and Meta.

### Development

Source is plain JavaScript on Node.js with the Apify SDK. Tests use Node's built-in test runner and mocked HTTP/services. `src/main.js` provides the Apify lifecycle; `collector.js` collects posts; `extractor.js` calls Gemini; `events.js` validates/reconciles events; `pipeline.js` handles cache, history and output.

`npm start` runs through the actual Apify SDK locally. Put input in `storage/key_value_stores/default/INPUT.json`. Local live Instagram collection also needs `APIFY_TOKEN`; the cloud injects a scoped token automatically. For the easiest credential-free check use `npm run demo`.

### References

- [Apify Instagram Post Scraper](https://apify.com/apify/instagram-post-scraper)
- [Gemini model](https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash-lite)
- [Gemini image understanding](https://ai.google.dev/gemini-api/docs/image-understanding)
- [Gemini structured output](https://ai.google.dev/gemini-api/docs/structured-output)
- [Apify permissions](https://docs.apify.com/actors/development/permissions)
- [Apify deployment](https://docs.apify.com/actors/development/deployment)
- [Apify pay-per-event monetization](https://docs.apify.com/actors/monetize/pay-per-event)

# Actor input Schema

## `source` (type: `string`):

Start with Demo. Select Instagram for live profiles, or Dataset to read a previous Instagram Post Scraper dataset.

## `usernames` (type: `array`):

For Instagram mode only. Up to 10 public venue or organizer profiles. Example: @your_venue or https://www.instagram.com/your_venue/. No passwords or Instagram cookies.

## `datasetId` (type: `string`):

For Dataset mode only. Select a dataset with fields such as id, caption, timestamp, displayUrl, images and childPosts. The Actor reads at most maxTotalPosts raw rows.

## `geminiApiKey` (type: `string`):

Required for live modes, unless GEMINI_API_KEY is set in your private Actor environment. Get a key from Google AI Studio. Captions and selected images are sent to Gemini. Never publish a shared owner-funded key.

## `defaultTimezone` (type: `string`):

IANA timezone, for example Europe/Rome or America/New_York. Used when the source provides no timezone and flagged as an assumption. Use separate monitors for different regions.

## `maxPostsPerProfile` (type: `integer`):

Upper bound requested from each profile. Automatically lowered when needed to stay within maxTotalPosts across all profiles. Recent posts may include non-events or videos.

## `maxTotalPosts` (type: `integer`):

Maximum raw dataset rows inspected per run, including skipped videos and errors. In Instagram mode must be at least the number of profiles.

## `maxImagesPerPost` (type: `integer`):

Poster/carousel images analyzed per post. Truncated carousels are flagged. Lower this to reduce image-analysis costs. Videos and Stories are not analyzed.

## `maxEventsPerPost` (type: `integer`):

Maximum extracted occurrences per post. Dense monthly calendars may exceed this limit; inspect source and review flags.

## `scraperBudgetCents` (type: `integer`):

50 means USD 0.50. Applied as maxTotalChargeUsd to the upstream pay-per-event scraper. It does not cap Gemini, this Actor's fee or total platform usage.

## `monitorName` (type: `string`):

Keep this stable across scheduled runs. Use a different name for a fresh monitor. History is isolated by Actor, source profiles/dataset, model, timezone and extraction limits. Avoid overlapping runs for the same monitor.

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

First run returns all eligible events. Later runs can return an empty dataset when nothing changed. Disable to return all events observed in the current post window.

## `includePastEvents` (type: `boolean`):

Compare dates in the event timezone. Undated records stay visible for review. Explicit cancellations and postponements remain eligible.

## `forceReprocess` (type: `boolean`):

Bypass the successful-analysis cache. This makes new Gemini calls and can incur a post-analyzed fee again. Use when checking extraction quality or changing source images.

## `geminiModel` (type: `string`):

An image-capable Gemini model supporting structured JSON output. Your API key must have access. Default checked against Google's model documentation on 2026-10-29.

## Actor input object example

```json
{
  "source": "demo",
  "usernames": [],
  "defaultTimezone": "UTC",
  "maxPostsPerProfile": 5,
  "maxTotalPosts": 50,
  "maxImagesPerPost": 4,
  "maxEventsPerPost": 10,
  "scraperBudgetCents": 50,
  "monitorName": "instagram-events-history",
  "onlyChanges": true,
  "includePastEvents": false,
  "forceReprocess": false,
  "geminiModel": "gemini-3.5-flash-lite"
}
```

# Actor output Schema

## `events` (type: `string`):

No description

## `csv` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("renaissant_vibraphone/instagram-event-extractor").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("renaissant_vibraphone/instagram-event-extractor").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 '{}' |
apify call renaissant_vibraphone/instagram-event-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,renaissant_vibraphone/instagram-event-extractor"
        }
    }
}
```

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/r1P7KJrvnnCJihFfp/builds/iqmzP5pI3ZpielcjV/openapi.json
