# Dataset Diff & Change Detector (`nerolabs/dataset-diff-detector`) Actor

Returns what was added, removed or changed between two Apify datasets, files or Google Sheets by URL, or JSON arrays, matched by key, field by field, or since its own last run (snapshot mode). CSV/Excel diff report, named-dataset change log, webhook delivery. Agent-ready: pay per event (x402, MCP).

- **URL**: https://apify.com/nerolabs/dataset-diff-detector.md
- **Developed by:** [Adam Pearce](https://apify.com/nerolabs) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 difference detecteds

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?

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

**Compare two datasets and get exactly what changed**, no manual spreadsheet comparison, no writing your own diff script. Point it at two Apify datasets, two CSV, Excel or JSON files or Google Sheets by URL, or two pasted JSON arrays (any mix), give it a key field like `id`, `sku`, or `email`, and it tells you precisely which rows were **added**, **removed**, or **changed**, down to which fields on each row actually moved. Works with the output of any scraper, any monitor Actor, or your own exported data. Or skip the 'old' side entirely: give the comparison a **snapshot name** and the Actor remembers the last dataset (or sheet) it saw, so every run reports **what changed since the last run**, appended to a running change log and POSTed to your webhook if you want.

### Why use Dataset Diff & Change Detector?

If you scrape or export the same source more than once, you already have this problem: two big JSON files, and no fast way to answer "what actually changed since last time?" without eyeballing thousands of rows.

- **Re-scraping the same source on a schedule?** Feed it yesterday's dataset and today's, and skip building your own comparison logic.
- **Reconciling two exports from different systems?** Match them on a shared key (email, SKU, ID) and see exactly where they diverge.
- **Auditing a data migration?** Confirm nothing silently dropped or changed during a transfer.
- **Want only what is new since yesterday's scrape, automatically?** Set a snapshot name, chain this Actor after the scraper, and each run returns just the additions, removals and changes since the previous run, no dataset IDs to juggle.
- **Watching a Google Sheet or a published CSV that other people edit?** Put its link in **New file URL** with a snapshot name and schedule the run: every edit since last time lands in the dataset and, with a **Webhook URL**, in Slack.
- **QA-checking a scraper you just changed?** Diff its old output against its new output to see the real impact of your change, not just "does it run."

No scraping involved at all, it only processes data you already have, so there's nothing to break when a website changes and nothing to worry about on data-source terms.

### How to use it

1. Pick your two snapshots, each from any source: an existing Apify dataset (**Old dataset** / **New dataset**), a public file or Google Sheet link (**Old file URL** / **New file URL**), or a pasted JSON array (**Old data (inline)** / **New data (inline)**). Mixing is fine.
2. Set **Key field(s)** to whatever uniquely identifies a row (e.g. `sku`, `id`, `email`). This is what lets the Actor tell "this row changed" apart from "this row was removed and a different one was added".
3. Optionally list fields to ignore (like a `scrapedAt` timestamp that always differs) under **Fields to ignore**, so it doesn't flag every row as changed for no real reason.
4. Pick where the result goes: this run's dataset (always), a **named dataset** that builds a running change log, a **CSV or Excel report**, or a **webhook**. Run it and check the **added** / **removed** / **changed** rows.

### Bringing in a file or a Google Sheet

Set **Old file URL** and/or **New file URL** to any public link. The format is detected from the extension, the content type and the content itself, or force it with **File format**:

- **CSV / TSV**: header row required; quotes, embedded newlines and semicolon or tab delimiters are handled.
- **Excel (.xlsx)**: the first sheet, header row in row 1; dates come out as ISO strings, formulas as their computed values.
- **JSON**: an array, an object wrapping an array (`{"items": [...]}`, `{"data": [...]}`), or one object per line (JSON Lines).
- **Google Sheets**: paste the normal sheet link. Set sharing to "Anyone with the link can view" (or File > Share > Publish to the web); the Actor converts it to the CSV export link for you, including the specific tab if the link carries a `gid`.

Up to 100 MB per side per run. Downloading a file is never charged. Values read from a CSV or sheet are text, so when the other side is a dataset with real numbers, list the numeric fields under **Fields to ignore** or compare like with like (two sheets, or two datasets) to avoid `19.99` vs `"19.99"` showing as a change.

### Input

- **Old dataset / New dataset**: pick existing Apify datasets via the resource picker (limited-permissions safe, it can only read the dataset you point at).
- **Old file URL / New file URL**: a CSV, TSV, Excel, JSON or JSON Lines file, or a Google Sheet link, for either snapshot (see above). **File format** overrides auto-detection if ever needed.
- **Old data / New data (inline)**: paste a JSON array directly instead, for one-off comparisons.
- **Key field(s)**: the field(s) that identify a row across both snapshots. Leave empty to match by full row content instead (still works, but then a changed row shows as a remove+add pair, not a single "changed" row).
- **Fields to compare**: optionally restrict comparison to specific fields only.
- **Fields to ignore**: field names to exclude from comparison (timestamps, run IDs, anything that always differs).
- **Include unchanged rows in the output**: off by default, since most people only want the deltas.
- **Also append to a named dataset**: optional; every run's diff rows are also appended to a dataset of that name in your account (created on the first run), a running change log across scheduled runs. Not charged again.
- **Export diff report as file**: optionally generate a real downloadable CSV and/or Excel file on top of the dataset.
- **Webhook URL**: optional; POST the diff rows and counts to your endpoint on every run (see below).
- **Snapshot name**: optional. Name the comparison and the Actor keeps the 'new' snapshot under that name for next time, so you only ever supply the new data. See the next section.

### Changes since the last run (snapshot mode)

Most people re-scrape the same source on a schedule and only care about the delta. Snapshot mode does that without you keeping track of dataset IDs:

1. Set **Snapshot name** to something like `shop-prices`. Leave **Old dataset** and **Old data** empty.
2. Point **New dataset** at today's dataset (or paste **New data**). Set **Key field(s)** as usual.
3. Run it. The **first run saves the baseline and reports nothing**. Every later run with the same name compares against the saved snapshot, reports only the added, removed and changed rows, and then saves the new snapshot for next time.

To chain it after a scraper, add an integration on the scraper's Actor or task ("Run Actor" on finish) that starts this Actor with an input like:

```json
{
  "snapshotName": "shop-prices",
  "newDatasetId": "{{resource.defaultDatasetId}}",
  "keyFields": ["sku"],
  "ignoreFields": ["scrapedAt"]
}
```

Useful details:

- **Seed the baseline without a wasted run**: fill **Old dataset** or **Old data** on the first run together with the snapshot name. That run compares as normal and saves the new snapshot.
- **A failed scrape cannot wipe your baseline**: if the new snapshot has zero rows, nothing is compared, nothing is charged beyond the run start, and the saved snapshot is kept. To reset a name on purpose, run once with **Reset snapshot** turned on.
- Snapshots are stored gzipped in a key-value store named `nerolabs-dataset-diff-snapshots` in your own Apify account, created by this Actor, under the keys `<name>__meta` and `<name>__part-0000`, `__part-0001`, and so on. Delete those records, or use a new name, to start again. The Actor keeps its limited permissions: it only ever touches that store, its own default storages, and the datasets you point it at.
- The `DIFF_SUMMARY` record reports the `mode` (`two-snapshots`, `since-last-run`, `since-last-run-baseline` or `since-last-run-skipped-empty`) and, for snapshot runs, when the previous snapshot was saved and how many rows the new one holds.

### Output

One row per difference, for example:

```json
{
  "status": "changed",
  "key": { "sku": "A100" },
  "oldValues": { "sku": "A100", "title": "Blue Widget", "price": 19.99, "stock": 42 },
  "newValues": { "sku": "A100", "title": "Blue Widget", "price": 17.99, "stock": 30 },
  "changedFields": ["price", "stock"]
}
```

`status` is one of `added`, `removed`, `changed`, or (if you turned that option on) `unchanged`. You can download the dataset in various formats such as JSON, CSV, or Excel directly from the Output tab, or turn on the built-in export for a ready-to-open diff report file. With **Also append to a named dataset** set, the same rows are appended to a dataset of that name in your account, so a scheduled watch builds one running change log. The `DIFF_SUMMARY` record also says where each snapshot came from (`dataset`, `fileUrl`, `inline` or `saved-snapshot`).

### Webhook destination

Set **Webhook URL** in the input and the diff rows (plus the added/removed/changed counts, the snapshot details and any download links) are POSTed there as JSON the instant the run finishes. The delivery happens on every run, including a first baseline run and a run with no changes, so a scheduled watch always reports back to Slack, Zapier, Make, n8n or your own API and your pipeline never wonders whether the run happened. A failed or unreachable webhook never breaks the run, it's reported as a warning in the output and costs nothing. Charged only on a confirmed delivery (see Pricing).

### Pricing

Pay-per-event, anchored the same way as every low-maintenance data tool in this line: you're charged **$0.005 per real difference found** (an added, removed, or changed row), **$0.0005 per unchanged row confirmed** (only if you turn that option on), **$0.01 per file export**, and **$0.02 per confirmed webhook delivery** (only when your endpoint responds 2xx; a failed delivery costs nothing), effective 21 September 2026 and free before that. Comparing two 1,000-row datasets where 50 rows actually changed costs about $0.25, not $5, since you're only billed for the parts of the answer that are actually useful. Downloading a file by URL, saving a snapshot and appending to a named dataset are never charged. From 21 September 2026, Apify Store discounts apply on every event: 10% off for Bronze, 20% for Silver and 30% for Gold accounts. There's no separate platform-usage charge beyond the small per-GB run-start fee (the platform default).

### Tips

- Always set **Key field(s)** if your data has any kind of stable identifier. Without one, the Actor still works, but "changed" rows show up as a remove+add pair instead, which is technically correct but less useful for spotting exactly what moved.
- Add any always-changing field (timestamps, scrape IDs, "last checked" dates) to **Fields to ignore**, otherwise every single row will look "changed" every time.
- Duplicate key values within one snapshot are handled gracefully (the last-seen row wins) and flagged in the run's warnings, so you'll know if your chosen key field isn't actually unique.

### Works with the rest of the Nero Labs dataset toolkit

- [Dataset Cleaner & Exporter](https://apify.com/nerolabs/dataset-cleaner-exporter): dedupe (exact, normalized or fuzzy), flatten nested JSON, clean emails, phones and URLs, then export CSV or Excel.
- [Dataset Filter & Transform](https://apify.com/nerolabs/dataset-filter-transform): keep the rows you want and reshape the fields (dates, replace, split, hash, 25 ops), sort, dedupe, limit.
- [Dataset Join & Merge](https://apify.com/nerolabs/dataset-join-merge): VLOOKUP-style joins and unions across two datasets, files or Google Sheets on a key field.
- [Dataset Aggregate, Group By & Pivot](https://apify.com/nerolabs/dataset-aggregate-pivot): counts, sums, averages and pivot tables per group.
- **Dataset Diff & Change Detector** (this one): what was added, removed or changed since last time.
- [Dataset AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich): add LLM-generated columns (classify, extract, summarise) to every row, no API key needed.
- [Dataset Charts & Report](https://apify.com/nerolabs/dataset-charts-report): chart images (PNG, SVG) and a PDF or HTML report from any data.
- [Dataset to Postgres, Supabase & MySQL](https://apify.com/nerolabs/dataset-to-database): write the rows straight into a database table, creating it if needed.
- [Dataset to REST API](https://apify.com/nerolabs/dataset-to-rest-api): send every row to any API as its own request, with templating and auth presets.
- [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner): chain several of these together in one run, each step fed the previous step's dataset.

A common pipeline: a scraper, then Cleaner, then Filter & Transform, then Join to enrich from a sheet, then Aggregate for the weekly summary, with Diff watching what changed and Charts & Report turning the numbers into the Monday PDF. Pipeline Runner runs that whole chain in one call.

### FAQ

**Does this work with any Apify dataset?** Yes, point "Old dataset" and "New dataset" at any two dataset IDs from your own account, including the output of another Actor's run.

**How do I get only what changed since my last scrape, without saving dataset IDs?** Set a **Snapshot name**. The Actor keeps the last snapshot for you and each run reports the delta since the previous one. See "Changes since the last run" above.

**Where are the snapshots kept and can I delete them?** In a key-value store called `nerolabs-dataset-diff-snapshots` in your own account, one `__meta` record plus gzipped `__part-NNNN` records per name. Delete them any time, or start fresh with a new name.

**What if I don't have two dataset IDs, just two JSON files?** Use the inline "Old data" / "New data" fields instead and paste the arrays directly.

**Can I watch a Google Sheet for changes?** Yes. Paste the sheet link into **New file URL**, set a **Snapshot name** and put the Actor on a schedule; each run reports what changed in the sheet since the previous run. If the link gives an HTTP 401 or 403, the sheet isn't public: set sharing to "Anyone with the link can view", or use File > Share > Publish to the web and paste that CSV link.

**Is my data safe?** This Actor only reads the sources you point it at and writes to your own Apify account's storage (the run's output, its snapshot store, and a named dataset if you set one). It doesn't scrape and doesn't retain anything else. The one place data leaves your account is a **Webhook URL** you set yourself, which receives only that run's result.

Found a bug or want a feature? Use the Issues tab, replies come from a real person, usually within hours.

# Actor input Schema

## `oldDatasetId` (type: `string`):

Pick an existing Apify dataset holding the earlier version of your data. Use this OR 'Old file URL' OR 'Old data (inline)' below; leave all three empty when a 'Snapshot name' is set further down. Declaring it this way is what lets this Actor run with limited permissions: it may read the dataset you point at, and nothing else on your account.

## `oldFileUrl` (type: `string`):

Instead of a dataset, download the 'before' snapshot from a public link: a CSV or TSV file, an Excel .xlsx file (first sheet, header row), a JSON array or JSON Lines file, or a Google Sheet (paste the normal sheet link, sharing set to 'Anyone with the link can view'). The format is detected automatically. Up to 100 MB. Never charged. Ignored when 'Old dataset' is set.

## `oldData` (type: `array`):

A JSON array of records for the 'before' snapshot, for ad-hoc data instead of a dataset ID or file URL.

## `newDatasetId` (type: `string`):

Pick an existing Apify dataset holding the newer version of your data. Use this OR 'New file URL' OR 'New data (inline)' below.

## `newFileUrl` (type: `string`):

Instead of a dataset, download the 'after' snapshot from a public link (same formats as above). Combined with a 'Snapshot name' below this watches a Google Sheet or a published CSV for changes: every run reports only what changed since the last one. Never charged. Ignored when 'New dataset' is set.

## `newData` (type: `array`):

A JSON array of records for the 'after' snapshot, for ad-hoc data instead of a dataset ID or file URL.

## `fileFormat` (type: `string`):

Only needed if automatic detection gets a file URL's format wrong. Applies to both file URLs.

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

Optional. 1 to 63 letters, digits, dashes or underscores, for example 'shop-prices'. With a name set you can leave 'Old dataset' and 'Old data' empty: the first run saves the baseline and reports nothing, every later run compares the new data against the saved snapshot and then replaces it. Fill 'Old dataset' or 'Old data' as well on the first run to seed the baseline without a wasted run. An empty new snapshot never overwrites a saved baseline. Snapshots live in a key-value store named 'nerolabs-dataset-diff-snapshots' in your own account.

## `resetSnapshot` (type: `boolean`):

Optional. Turn on for one run to forget whatever is saved under the snapshot name above: this run compares nothing (unless you also supply an old snapshot) and saves the new data as a fresh baseline. Turn it off again afterwards.

## `keyFields` (type: `array`):

The field name(s) that uniquely identify a row across both snapshots (e.g. 'sku', 'id', or 'email'). This is what lets the Actor tell 'this row changed' apart from 'this row was removed and a different one was added'. Leave empty to match rows by their full content instead (fine for simple lists, but then any change to any field makes a row look like a remove+add pair rather than a 'changed' row).

## `compareFields` (type: `array`):

Optional. Only compare these specific fields when deciding if a matched row changed. Leave empty to compare every field except any listed in 'Fields to ignore' below.

## `ignoreFields` (type: `array`):

Optional. Field names to exclude from comparison, useful for things like a 'scrapedAt' or 'lastChecked' timestamp that always differs between runs and would otherwise mark every row as changed.

## `includeUnchanged` (type: `boolean`):

When on, rows present in both snapshots with no real difference are also written to the output dataset (charged at a much lower rate, see Pricing). Off by default, since a diff tool's whole point is usually just the things that changed.

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

Safety cap on how many rows from the larger of the two snapshots to process. Leave at 0 for no cap (up to the Actor's own 100,000-row hard limit).

## `outputDatasetName` (type: `string`):

Optional. A name (3 to 63 letters, digits or hyphens, e.g. 'price-change-log'). Every run's added, removed and changed rows are appended to a dataset with this name in your account, created on the first run, so a scheduled watch builds one running change log instead of one dataset per run. Not charged.

## `exportFormats` (type: `array`):

Optional. Generate a real downloadable diff report on top of the dataset. Leave empty to skip file export entirely.

## `webhookUrl` (type: `string`):

Optional. If set, the diff rows (plus the added/removed/changed counts, snapshot details and download links) are POSTed here as JSON on every run, including a run with no changes, so a scheduled watch always reports back to Slack, Zapier, Make, n8n or your own endpoint. Only charged when the endpoint actually confirms receipt (HTTP 2xx); a failed delivery is reported as a warning in the run's output and costs nothing.

## Actor input object example

```json
{
  "oldData": [
    {
      "sku": "A100",
      "title": "Blue Widget",
      "price": 19.99,
      "stock": 42
    },
    {
      "sku": "A101",
      "title": "Red Widget",
      "price": 24.99,
      "stock": 0
    },
    {
      "sku": "A102",
      "title": "Green Widget",
      "price": 15.5,
      "stock": 8
    }
  ],
  "newData": [
    {
      "sku": "A100",
      "title": "Blue Widget",
      "price": 17.99,
      "stock": 30
    },
    {
      "sku": "A102",
      "title": "Green Widget",
      "price": 15.5,
      "stock": 8
    },
    {
      "sku": "A103",
      "title": "Yellow Widget",
      "price": 12,
      "stock": 100
    }
  ],
  "fileFormat": "auto",
  "resetSnapshot": false,
  "keyFields": [
    "sku"
  ],
  "ignoreFields": [
    "scrapedAt",
    "lastChecked",
    "timestamp"
  ],
  "includeUnchanged": false,
  "maxItems": 0,
  "exportFormats": []
}
```

# Actor output Schema

## `diffRows` (type: `string`):

One row per difference found (added, removed, changed), plus unchanged rows if that option was enabled.

## `csvFile` (type: `string`):

A ready-to-open CSV file of the diff report, if requested.

## `xlsxFile` (type: `string`):

A ready-to-open Excel (.xlsx) file of the diff report, if requested.

## `diffSummary` (type: `string`):

Counts of added, removed, changed and unchanged rows, and any warnings from this run.

# 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 = {
    "oldData": [
        {
            "sku": "A100",
            "title": "Blue Widget",
            "price": 19.99,
            "stock": 42
        },
        {
            "sku": "A101",
            "title": "Red Widget",
            "price": 24.99,
            "stock": 0
        },
        {
            "sku": "A102",
            "title": "Green Widget",
            "price": 15.5,
            "stock": 8
        }
    ],
    "newData": [
        {
            "sku": "A100",
            "title": "Blue Widget",
            "price": 17.99,
            "stock": 30
        },
        {
            "sku": "A102",
            "title": "Green Widget",
            "price": 15.5,
            "stock": 8
        },
        {
            "sku": "A103",
            "title": "Yellow Widget",
            "price": 12,
            "stock": 100
        }
    ],
    "keyFields": [
        "sku"
    ],
    "ignoreFields": [
        "scrapedAt",
        "lastChecked",
        "timestamp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/dataset-diff-detector").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 = {
    "oldData": [
        {
            "sku": "A100",
            "title": "Blue Widget",
            "price": 19.99,
            "stock": 42,
        },
        {
            "sku": "A101",
            "title": "Red Widget",
            "price": 24.99,
            "stock": 0,
        },
        {
            "sku": "A102",
            "title": "Green Widget",
            "price": 15.5,
            "stock": 8,
        },
    ],
    "newData": [
        {
            "sku": "A100",
            "title": "Blue Widget",
            "price": 17.99,
            "stock": 30,
        },
        {
            "sku": "A102",
            "title": "Green Widget",
            "price": 15.5,
            "stock": 8,
        },
        {
            "sku": "A103",
            "title": "Yellow Widget",
            "price": 12,
            "stock": 100,
        },
    ],
    "keyFields": ["sku"],
    "ignoreFields": [
        "scrapedAt",
        "lastChecked",
        "timestamp",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/dataset-diff-detector").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 '{
  "oldData": [
    {
      "sku": "A100",
      "title": "Blue Widget",
      "price": 19.99,
      "stock": 42
    },
    {
      "sku": "A101",
      "title": "Red Widget",
      "price": 24.99,
      "stock": 0
    },
    {
      "sku": "A102",
      "title": "Green Widget",
      "price": 15.5,
      "stock": 8
    }
  ],
  "newData": [
    {
      "sku": "A100",
      "title": "Blue Widget",
      "price": 17.99,
      "stock": 30
    },
    {
      "sku": "A102",
      "title": "Green Widget",
      "price": 15.5,
      "stock": 8
    },
    {
      "sku": "A103",
      "title": "Yellow Widget",
      "price": 12,
      "stock": 100
    }
  ],
  "keyFields": [
    "sku"
  ],
  "ignoreFields": [
    "scrapedAt",
    "lastChecked",
    "timestamp"
  ]
}' |
apify call nerolabs/dataset-diff-detector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nerolabs/dataset-diff-detector"
        }
    }
}

```

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/LSGrUPrLWIt2rmhho/builds/STEZzJ5IdHIiQM5R0/openapi.json
