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

Returns what was added, removed or changed between two Apify datasets, files, Google Sheets or JSON arrays, matched by key, or since its own last run. Only-new-items mode returns just rows never seen on any earlier run. CSV/Excel report, change log, webhook. 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
- **Features:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 differences

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

**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.
- **Only want the new items since your last scrape, never the same row twice?** Tick **Only new items**: each run returns just the rows it has never seen before under that snapshot name, even if a row disappeared for a while and came back. Point it at the Google Maps, jobs, listings or leads dataset your scraper made and feed only the fresh rows into your CRM or Sheet.
- **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**: when a snapshot is already saved under the name, no old dataset, file or data is supplied, and the new snapshot has zero rows, nothing is compared, no rows or files are charged, and the saved snapshot is kept. A **Webhook URL**, if set, is still delivered and charged. The guard does not apply when you also supply an old snapshot on that run: an empty new side is then compared as normal, every old row is reported and charged as removed, and the empty result is saved as the new baseline. To reset a name on purpose, run once with **Reset snapshot** turned on.
- **Max rows to process** caps each side separately (the first N rows of the new data, and of the old data if you supply it) but never caps a snapshot saved by an earlier run. If the cap is smaller than the saved snapshot, rows past the cap can be reported and charged as removed, and the shorter set is saved as the next baseline, so keep the cap the same from run to run or leave it at 0.
- 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.

### Only new items since your last scrape (remember every row ever seen)

Plain snapshot mode compares against the **previous** run only, so a row that drops out of one scrape and reappears in the next shows up as "added" again. If what you want is **only new items, never repeats**, for example new Google Maps places, new job ads or new listings to push into a CRM, turn on **Only new items (remember every row ever seen)**:

1. Set **Snapshot name** (for example `maps-leads`) and **Key field(s)** (a URL, place ID or listing ID works best).
2. Tick **Only new items** and point **New dataset** at the scraper's output, or chain it after the scraper as shown above with `"onlyNewItems": true` added.
3. The first run saves a baseline and returns nothing. Every later run returns **only rows whose key was never seen on any earlier run** under that name. Removed and changed rows are left out, and so are rows that come back after a gap.

Each new row comes back with `status: "added"` and the full original row in `newValues`, charged as one difference per new row; rows already seen cost nothing. Without key fields, a row is matched on its full content, so a changed price would count as a new item: set a key. The memory holds up to 500,000 rows per name, forgetting the oldest after that, and stores only short hashes of each key, never the row content, under `<name>__seen-meta` and `<name>__seen-part-0000` in the same `nerolabs-dataset-diff-snapshots` store. **Reset snapshot** clears it. Turning the option on for a name that already has a saved snapshot starts the memory from that snapshot. `DIFF_SUMMARY` adds an `onlyNewItems` block with how many rows are remembered, how many were new this run and how many were already seen.

### 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, a run with no changes and a run skipped because the new snapshot was empty, 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 on every confirmed delivery, including on those baseline, no-change and empty runs (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, 26 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. When a baseline is saved and no old snapshot is supplied, an empty new snapshot is skipped and the baseline is kept (a webhook, if set, is still delivered and charged). If you also supply an old snapshot on that run, an empty new side is compared as normal, every old row is charged as removed, and the empty set is saved. 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.

## `onlyNewItems` (type: `boolean`):

Optional, off by default. Needs 'Snapshot name'. Returns only rows never seen on ANY earlier run under that name, so each scheduled run after a scraper gives you just the new items: a row that disappears and later comes back is not reported again, and removed or changed rows are left out. Rows come back as 'added' with the full row in 'newValues', charged per new row only. Set 'Key field(s)' (for example a URL or place ID) so a changed price does not count as a new item. The first run with a new name saves a baseline and returns nothing. Up to 500,000 rows are remembered per name (only short hashes of the key are stored, never row content); 'Reset snapshot' clears the memory.

## `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 applied to each side separately: only the first N rows of the new data, and of the old data if you supply it, are loaded. A snapshot saved by an earlier run is never capped, so a cap smaller than that snapshot can report rows past the cap as removed (charged) and saves the shorter set as the next baseline. Leave at 0 for no cap (up to the Actor's own 100,000-rows-per-side 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 baseline run, a run with no changes and a run skipped for an empty new snapshot, so a scheduled watch always reports back to Slack, Zapier, Make, n8n or your own endpoint. Charged on every run where the endpoint actually confirms receipt (HTTP 2xx), those runs included; 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,
  "onlyNewItems": 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/kXZLTb7Bld2LGLOUt/openapi.json
