# Change & Alert Engine - Webhook on Change (`darknezz/change-alert-engine`) Actor

Generic change-detection engine: poll any URL, JSON feed or RSS/Atom feed, diff it against the last seen state (key-value store), and push ONLY the changes to the dataset and/or a webhook. Each change carries {item\_id, change\_type, before, after, changed\_at}.

- **URL**: https://apify.com/darknezz/change-alert-engine.md
- **Developed by:** [Oaida Adrian](https://apify.com/darknezz) (community)
- **Categories:** Automation, Developer tools, News
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 change emitteds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Change & Alert Engine — Webhook on Change (any URL / RSS / JSON)

A generic **change-detection engine**: point it at any URL — a JSON feed, an
RSS/Atom feed, or a plain HTML page — and it diffs each poll against the last
seen state (stored in the actor's key-value store) and emits **ONLY the
changes**, with `before` and `after` values, to the dataset and/or your
webhook.

Turn any batch poller into a push actor: no more re-downloading an entire
feed to find what's new — the actor tells you exactly which items changed,
and nothing else.

### Why a change & alert engine?

- **Event-driven beats batch.** Most data jobs re-pull the whole source every
  run and waste time/money on unchanged data. A delta engine wakes up, checks
  what actually moved, and pushes only the diff.
- **One actor, every source.** Recalls lists, government notices, price
  tables, news feeds, release notes — if it's a URL, this engine watches it.
- **Structured before/after.** Consumers get `{item_id, change_type, before,
  after, changed_at}` — enough to render "what changed" without keeping their
  own copy of the previous state.

### How it works

1. **Poll** — the actor fetches `sourceUrl` once per run (schedule it with
   your chosen `interval`; the actor is a single-shot poll).
2. **Parse** — content is sniffed automatically:
   - **JSON** — arrays are used directly; object feeds auto-detect common
     list keys (`items`, `results`, `data`, `records`, `entries`…) or use
     `itemsPath` for a dot path (`data.records`).
   - **RSS / Atom** — items are parsed with stable ids (guid or link).
   - **HTML** — the page becomes a single monitored item keyed on a content
     hash (page-level change detection).
3. **Diff** — each item gets a stable `item_id` (`idField`, natural keys, or
   a content hash fallback) and a content hash. The current snapshot is
   compared to the persisted one:
   - new id → `added` (before `null`)
   - same id, different hash → `modified` (before + after full values)
   - id missing from the current fetch → `removed` (only with
     `includeRemovals: true`)
4. **Emit** — changed items are pushed to the dataset, one per item, and
   (optionally) one batched webhook POST is sent per run that has changes.
   Nothing is emitted on a clean poll.
5. **Persist** — the new state is saved to the key-value store so the next
   poll diffs against it.

### Input

| Field | Type | Description |
|---|---|---|
| `sourceUrl` | string | URL to monitor (JSON feed, RSS/Atom, or HTML page). When empty, the actor polls the Hacker News front page feed by default. |
| `interval` | string | Informational — how often you schedule the actor (e.g. `15m`, `1h`, `0 9 * * *`). Included in the webhook payload. |
| `webhook` | string | Optional URL. One POST with the full changes payload per run that has changes; never fired on a clean run. |
| `dedupe` | bool | `true` (default): persist state and emit only diffs. `false`: emit the full snapshot every run without persisting. |
| `itemsPath` | string | Dot path to the item list inside a JSON object (e.g. `data.records`). Auto-detected when empty. |
| `idField` | string | Field to use as the stable item id. Natural keys (`id`, `guid`, `itemId`, `permalink`, `slug`, `link`, `url`, `title`, `name`) are tried when empty. |
| `ignoreFields` | string | Comma-separated top-level fields stripped before hashing (e.g. `updatedAt`) so volatile metadata doesn't cause false modifications. |
| `includeRemovals` | bool | Report items that disappeared from the feed as `removed` (after `null`). Off by default. |
| `maxItems` | int | Max items considered per poll (default 500, max 20 000). |

Example input:

```json
{
  "sourceUrl": "https://recalls-rappels.canada.ca/en/search?search_api_fulltext=",
  "interval": "15m",
  "webhook": "https://hooks.example.com/alerts",
  "dedupe": true
}
```

### Output

One dataset item per change:

| Field | Description |
|---|---|
| `item_id` | Stable id of the changed item. |
| `change_type` | `added`, `modified`, or `removed`. |
| `before` | Previous value (full item) — `null` for additions. |
| `after` | New value (full item) — `null` for removals. |
| `changed_at` | When the change was detected (ISO-8601 UTC). |
| `sourceUrl` / `polledAt` | Monitoring context. |

Webhook payload (one POST per run with changes):

```json
{
  "sourceUrl": "https://…",
  "polledAt": "2026-08-13T12:00:00.000000+00:00",
  "requestedInterval": "15m",
  "changeCount": 2,
  "changes": [
    {"item_id": "r1", "change_type": "modified",
     "before": {"id": "r1", "risk": "Fire"},
     "after":  {"id": "r1", "risk": "Fire + Shock"},
     "changed_at": "2026-08-13T12:00:00.000000+00:00"}
  ]
}
```

### Use cases

- **Government / safety monitors** — watch a recalls search page or a
  regulatory feed; get a webhook the moment a new notice appears.
- **Price & inventory alerts** — poll a product JSON endpoint; emit only rows
  whose price/stock changed, with old and new values side by side.
- **News / press-release watchers** — diff an RSS feed and forward only the
  new entries to a chat webhook.
- **Status pages** — monitor an HTML status page by content hash; you are
  notified only when the page actually changes.

### Delivery semantics

- **At-least-once.** State is persisted only after a successful webhook
  delivery (or immediately when no webhook is configured). If the POST fails,
  the run FAILS and the next run re-emits the same changes — an alert is
  never silently lost.
- **Within-run dedupe.** Duplicate item ids in one fetch collapse to the last
  occurrence, so a feed that repeats entries won't double-notify.

### Pricing

Pay per event — you only pay for what you extract:

- **apify-actor-start** — one-time charge per run.
- **result** — per change emitted (default primary event).

No monthly fee, no hidden costs. A clean poll (nothing changed) extracts
nothing and charges nothing beyond the actor start.

### Limitations

- The engine compares *current fetch vs last persisted fetch* — items that
  leave the feed are only reported with `includeRemovals: true` (off by
  default because first-page rotation is often noise).
- JSON items without any natural id fall back to a content hash as the id —
  with no stable id, any edit looks like an add+remove pair; set `idField`
  for clean `modified` detection.
- The actor polls once per run; the `interval` is your schedule on the actor
  (or an external scheduler). It does not stay resident between polls.
- Feeds with per-item volatile timestamps can look "modified" every poll —
  list those fields in `ignoreFields` to hash only what matters.

# Actor input Schema

## `sourceUrl` (type: `string`):

Any URL to monitor: a JSON feed (array or object), an RSS/Atom feed, or an HTML page (page-level change detection). When empty, the actor polls the Hacker News front page feed by default.

## `interval` (type: `string`):

How often you intend to schedule this actor (e.g. 15m, 1h, 0 9 \* \* \*). Used for the schedule you configure on the actor and included in the webhook payload; the actor itself polls once per run.

## `webhook` (type: `string`):

If set, one POST with the full changes payload is sent per run that has at least one change. No request is sent when nothing changed. If the POST fails, the run FAILS and state is NOT persisted, so the next run re-emits the changes (at-least-once delivery).

## `dedupe` (type: `boolean`):

true (default): persist the last seen state in the key-value store and emit only new/changed/removed items. false: emit the full snapshot on every run without persisting state.

## `itemsPath` (type: `string`):

Dot path to the item list inside a JSON object, e.g. data.items or results. When empty, common keys (items, results, data, records, entries...) are auto-detected; if none found the whole object is treated as one item.

## `idField` (type: `string`):

Field to use as the stable item id. When empty, common keys (id, guid, itemId, permalink, slug, link, url, title, name) are tried; if none exist the item hash is used (any edit looks like add+remove).

## `ignoreFields` (type: `string`):

Comma-separated top-level fields stripped before hashing (e.g. updatedAt, fetchedAt) so volatile metadata does not produce false modifications.

## `includeRemovals` (type: `boolean`):

When true, items that were in the last seen state but are missing from the current fetch are emitted as removed changes (after=null). Off by default because items rotating off a feed's first page are often not meaningful.

## `maxItems` (type: `integer`):

Maximum number of items to consider from the source on each poll.

## Actor input object example

```json
{
  "sourceUrl": "https://hnrss.org/frontpage",
  "interval": "15m",
  "webhook": "",
  "dedupe": true,
  "includeRemovals": false,
  "maxItems": 500
}
```

# Actor output Schema

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

No description

## `item_id` (type: `string`):

No description

## `change_type` (type: `string`):

No description

## `before` (type: `string`):

No description

## `after` (type: `string`):

No description

## `changed_at` (type: `string`):

No description

## `sourceUrl` (type: `string`):

No description

## `polledAt` (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 = {
    "sourceUrl": "https://hnrss.org/frontpage",
    "interval": "15m",
    "webhook": "",
    "itemsPath": "",
    "idField": "",
    "ignoreFields": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("darknezz/change-alert-engine").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 = {
    "sourceUrl": "https://hnrss.org/frontpage",
    "interval": "15m",
    "webhook": "",
    "itemsPath": "",
    "idField": "",
    "ignoreFields": "",
}

# Run the Actor and wait for it to finish
run = client.actor("darknezz/change-alert-engine").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 '{
  "sourceUrl": "https://hnrss.org/frontpage",
  "interval": "15m",
  "webhook": "",
  "itemsPath": "",
  "idField": "",
  "ignoreFields": ""
}' |
apify call darknezz/change-alert-engine --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,darknezz/change-alert-engine"
        }
    }
}

```

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/wuwlDsFWEG1GtU3Tx/builds/UFcvlpBxEFBmrhNLL/openapi.json
