# Product Data QA & Price/Stock Change Report (`gunggme/product-snapshot-qa`) Actor

Validate product feeds and compare price/stock changes in two Apify datasets or JSON snapshots. Flag incomplete catalogs before updating your baseline.

- **URL**: https://apify.com/gunggme/product-snapshot-qa.md
- **Developed by:** [Adam](https://apify.com/gunggme) (community)
- **Stats:** 2 total users, 1 monthly users, 87.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.25 / completed report

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

## Product Data QA & Price/Stock Change Report

**Product feed validation for developers running Apify product scrapers, pricing pipelines and ecommerce analytics agencies.** Compare Apify datasets or small JSON snapshots for catalog QA and exact price/stock diffs before updating your baseline.

An incomplete scrape can look like a price change, a stockout or a deleted product. This Actor flags quality problems and recommends **HOLD** when the next baseline needs review. You supply two snapshots from the same collection scope; your workflow decides what to do with the recommendation.

### Start in three steps

1. **Try the default demo.** Run the prefilled synthetic input below; no source Dataset or report event is needed.
2. **Compare your snapshots.** Choose `datasets` for two completed Apify Dataset IDs or `inline` for two small arrays. Map the product key and price/currency/stock fields; confirm completeness only after checking the collections.
3. **Read the report.** Open **Output → REPORT** and check `gate` and `baselineRecommendation`. Keep the previous approved baseline after REVIEW/BLOCKED. Check MANIFEST and the result records before automating downstream actions.

### What the result means

| Example | Result | What your workflow should do |
|---|---|---|
| Complete inline snapshot: A changes $100 → $90 and in stock → out of stock | PASS / ADVANCE; one changed product, one price change and one stock change | Consider advancing the baseline after your collection checks |
| Partial example below: B is absent from the current collection | REVIEW / HOLD; one `NOT_OBSERVED` product | Keep the previous baseline; absence is not proof of deletion or stockout |
| Identical valid snapshots | PASS / ADVANCE; zero changed products | A completed no-change report still counts as one report |

The partial example uses `maxMissingPercent: 100` to show REVIEW. With the default 20% threshold, that same 50% missing collection is BLOCKED / HOLD. Neither result advances the baseline.

### Price

**$0.25 per completed comparison report**, including platform usage in the listed customer price. PASS, REVIEW, BLOCKED and no-change reports each count as one report. There are no paid start or per-row events. The default `demo` has no report event; the copyable `inline` and `datasets` examples cost $0.25 when a report completes. Your upstream scraper and account services have their own costs.

### Before you start

You must already have two product snapshots. This Actor does not scrape websites, provide an instant Shopify tracker, match products by similarity, convert currencies, or decide that a missing product was deleted. It recommends a baseline action; it does not change your baseline itself.

Inputs are limited to 10,000 combined rows, 10 MiB of remote data, or a 1 MiB inline body. Source Datasets are read only. The n8n workflow draft is **UNVERIFIED** and is not a ready-to-run integration.

### Try a synthetic example

```json
{
  "mode": "demo",
  "monitorKey": "demo-catalog-usd",
  "previousComplete": true,
  "currentComplete": true
}
```

Demo uses fixed synthetic products and requests no `report-completed` event. It returns a PASS report with one price change and two stock changes across two products. Platform/account charges, where applicable, are separate from this statement.

Demo also writes one summary row to the run's default Dataset for Store health checks. It references the verified report fingerprint; the full report remains in the key-value store. Non-demo comparisons use only the key-value store outputs below. The prefilled completeness declarations apply to this synthetic example; verify them before comparing your own collection.

### Try the incomplete-collection example ($0.25)

```json
{
  "mode": "inline",
  "monitorKey": "example-partial",
  "previousComplete": true,
  "currentComplete": false,
  "maxMissingPercent": 100,
  "previousItems": [
    { "sku": "A", "price": "100", "currency": "USD", "inStock": true },
    { "sku": "B", "price": "200", "currency": "USD", "inStock": true }
  ],
  "currentItems": [{ "sku": "A", "price": "90", "currency": "USD", "inStock": false }]
}
```

Expected: REVIEW / HOLD, one unobserved product and one changed product. Keep the previous baseline; B is not reported as deleted. The 100% missing threshold here demonstrates a review case; the default 20% threshold would produce BLOCKED / HOLD for this input. Both are completed diagnostic reports and the report price applies.

### Compare your own data

Use `mode: "datasets"` with two different Dataset IDs, or `mode: "inline"` with two small arrays. The selected source Datasets are read only. One row must represent one product or variant. Both snapshots use the same field mappings.

#### Compare two Apify datasets

Select two completed snapshots from your own account in the **Previous Dataset** and **Current Dataset** fields. Replace the example IDs below; the Actor requests READ access only. Set both completeness declarations to true only when you have checked that both collections are complete.

```json
{
  "mode": "datasets",
  "monitorKey": "my-store-usd",
  "previousDatasetId": "REPLACE_PREVIOUS_DATASET_ID",
  "currentDatasetId": "REPLACE_CURRENT_DATASET_ID",
  "previousComplete": false,
  "currentComplete": false
}
```

With these conservative declarations, a valid comparison returns HOLD. Use the same store, region, currency, filters and field mappings on both sides. Missing or inaccessible IDs are rejected without a report event.

#### Compare small JSON snapshots ($0.25)

```json
{
  "mode": "inline",
  "monitorKey": "my-store-usd",
  "keyFields": ["sku"],
  "comparisonScope": "price_and_stock",
  "previousComplete": true,
  "currentComplete": true,
  "previousItems": [
    { "sku": "A", "price": "100.00", "currency": "USD", "inStock": true }
  ],
  "currentItems": [
    { "sku": "A", "price": "90.00", "currency": "USD", "inStock": false }
  ]
}
```

The observed price change has `before="100"`, `after="90"`, `delta="-10"` and `percent="-10"`. Both fields changed, but `changedProducts` is 1. This complete, valid input passes the default quality rules.

Only set the completeness declarations to true after checking your collection. They are caller declarations, not independently verified evidence.

#### No changes

```json
{
  "mode": "inline",
  "monitorKey": "example-no-change",
  "previousComplete": true,
  "currentComplete": true,
  "previousItems": [{ "sku": "A", "price": "100", "currency": "USD", "inStock": true }],
  "currentItems": [{ "sku": "A", "price": "100", "currency": "USD", "inStock": true }]
}
```

Expected: PASS / ADVANCE, zero changed products. This is a completed report and the report price applies.

### Results

Outputs are fixed records in the run's default key-value store:

| Record | Contents |
|---|---|
| REPORT | Quality gate, baseline recommendation, checked fields, settings, version and summary |
| CHANGES | Typed product keys, source row references and normalized changes |
| ISSUES | Issue codes, affected source rows and corrective guidance |
| CHANGES_CSV / ISSUES_CSV | Human-readable exports |
| MANIFEST | Completion marker, required records and SHA-256 fingerprints |
| BILLING_STATUS | Report-event request/reconciliation status |

Check `MANIFEST.reportReady` and the required records before consuming the report. A successful platform run does not by itself mean the data passed QA. JSON is the automation format. CSV identity/text fields use JSON-style rendering to reduce formula-injection risks; spreadsheet import and re-saving behavior varies.

### Quality gates and baselines

- **PASS / ADVANCE:** the performed checks passed, both snapshots were declared complete and no hold condition was found.
- **REVIEW / HOLD:** incomplete declarations, unobserved keys below the blocking threshold, invalid field values, unknown stock, currency mismatch, extreme price changes or an undefined percentage from a zero baseline.
- **BLOCKED / HOLD:** missing or duplicate identities, an empty current snapshot, no unambiguous matched keys, or excessive unobserved keys.

An empty previous snapshot is rejected. An empty current snapshot produces a completed BLOCKED diagnostic report. Neither case is interpreted as proof of product deletion.

Duplicate keys on either side are excluded from both sides. Missing percentage uses eligible previous keys, not the difference in row counts. At a zero missing threshold, zero missing keys passes; any missing key blocks.

A permanently unobserved product can keep the monitor on HOLD indefinitely. To intentionally change scope, pause the workflow and explicitly select a new baseline, recording the reason; use a new monitor key when appropriate. A manually selected baseline is not presented as an Actor-validated PASS.

### Supported values and limits

- Combined input: **10,000 rows** across both snapshots.
- Remote input: **10 MiB** decompressed response bytes in total, including metadata.
- Inline input: **1 MiB** serialized UTF-8 body, checked after the SDK loads the input.
- Exact identity: one or two string/safe-integer fields. String `"001"` and number `1` are different keys.
- Safe dot paths: up to four levels. Array indices, wildcards, code and prototype paths are unsupported.
- Prices: nonnegative plain decimals, up to 18 integer digits and 8 fractional digits. Decimal strings are recommended. Existing JSON-number precision loss cannot be recovered.
- Money output: decimal strings. Percentage display rounds to four decimal places; thresholds use the exact value before rounding.
- Currency: trimmed uppercase three-letter format. Registry membership and exchange conversion are not checked.
- Stock: `true`, `false`, `"in_stock"`, `"out_of_stock"`. Other values remain unknown. `price_only` skips stock inspection.
- Default thresholds: price alert 5%, extreme change review 50%, missing-key block 20%, with inclusive boundaries for actual changes/missing keys.

Prices with symbols, commas, exponent strings or locale-specific separators are not automatically corrected. Missing/null/empty values are not converted to zero or false. A zero baseline has no percentage change; `0→0` is unchanged and `0→positive` requires review.

Use 512 MiB and a run timeout no greater than 60 seconds. Inputs are rejected on limits or detected source changes; data is not silently truncated into a normal report. Metadata checks do not guarantee transactional snapshot consistency.

For synthetic demo mode only, the Actor also accepts the 300-second platform timeout used by Store automated tests. Its internal 60-second execution deadline still applies. Inline and Dataset comparisons require a platform timeout of 60 seconds or less.

### Report-event billing

The price is **$0.25 per completed report**, with platform usage included in the listed customer price. PASS, REVIEW, BLOCKED and no-change reports each count as one completed report. There are no paid start or per-row events. The Actor requests `report-completed` only after verifying the required results and MANIFEST were saved.

Demo, invalid input, unavailable sources, input-limit rejection and mandatory-result-save failures do not request this report event. Set a finite maximum run charge that permits one configured report event. Other paid event types are unsupported by this implementation.

If an event request or reconciliation is ambiguous, results remain available and `BILLING_UNCONFIRMED` is returned. The Actor does not blindly issue a fresh charge on same-run recovery. This is not a guarantee of settlement or distributed exactly-once behavior. Do not start a new paid run just because polling timed out; reconcile the existing run first.

### Privacy and automation

Only provide product/variant data you are permitted to process. Source data is not modified. Input and output can remain in Apify storage according to your account's retention settings. Raw product payloads, credentials and signed URLs are not intentionally logged. No LLM or external telemetry service is used by the comparison engine.

Keep the last PASS baseline after REVIEW/BLOCKED. Deduplicate collection events by source run ID and serialize processing per monitor. The accompanying n8n state-transition draft is **UNVERIFIED** and is not a ready-to-run hosted integration.

For issue reports, share the engine version, safe error code and a synthetic reproduction. Do not include credentials, personal data or signed URLs. Custom data transformations and managed workflows are outside the supported scope.

# Actor input Schema

## `mode` (type: `string`):

Demo uses fixed synthetic rows. Do not mix input modes.

## `monitorKey` (type: `string`):

Caller-defined shop, region and filter scope. Not independent completeness evidence.

## `previousDatasetId` (type: `string`):

Previously approved baseline. READ only.

## `currentDatasetId` (type: `string`):

Completed current source. Must differ from baseline.

## `previousItems` (type: `array`):

Inline only; nonempty baseline, object per product.

## `currentItems` (type: `array`):

Inline only. Empty produces BLOCKED diagnosis.

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

One or two exact, type-sensitive key fields.

## `priceField` (type: `string`):

Safe dot notation, 1–4 levels. Array indices and prototype paths are rejected at runtime.

## `currencyField` (type: `string`):

Safe dot notation, 1–4 levels. Array indices and prototype paths are rejected at runtime.

## `stockField` (type: `string`):

Safe dot notation, 1–4 levels. Array indices and prototype paths are rejected at runtime.

## `comparisonScope` (type: `string`):

price_only does not inspect stock.

## `previousComplete` (type: `boolean`):

Caller declaration only.

## `currentComplete` (type: `boolean`):

Caller declaration only.

## `priceAlertPercent` (type: `number`):

Inclusive exact absolute change threshold, before display rounding.

## `extremePriceChangePercent` (type: `number`):

Inclusive REVIEW threshold.

## `maxMissingPercent` (type: `number`):

Inclusive BLOCKED threshold if any key is unobserved. Zero allows no missing keys.

## Actor input object example

```json
{
  "mode": "demo",
  "monitorKey": "demo-catalog-usd",
  "keyFields": [
    "sku"
  ],
  "priceField": "price",
  "currencyField": "currency",
  "stockField": "inStock",
  "comparisonScope": "price_and_stock",
  "previousComplete": true,
  "currentComplete": true,
  "priceAlertPercent": 5,
  "extremePriceChangePercent": 50,
  "maxMissingPercent": 20
}
```

# Actor output Schema

## `REPORT` (type: `string`):

No description

## `CHANGES` (type: `string`):

No description

## `ISSUES` (type: `string`):

No description

## `CHANGES_CSV` (type: `string`):

No description

## `ISSUES_CSV` (type: `string`):

No description

## `MANIFEST` (type: `string`):

No description

## `BILLING_STATUS` (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 = {
    "mode": "demo",
    "monitorKey": "demo-catalog-usd",
    "previousComplete": true,
    "currentComplete": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("gunggme/product-snapshot-qa").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 = {
    "mode": "demo",
    "monitorKey": "demo-catalog-usd",
    "previousComplete": True,
    "currentComplete": True,
}

# Run the Actor and wait for it to finish
run = client.actor("gunggme/product-snapshot-qa").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 '{
  "mode": "demo",
  "monitorKey": "demo-catalog-usd",
  "previousComplete": true,
  "currentComplete": true
}' |
apify call gunggme/product-snapshot-qa --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,gunggme/product-snapshot-qa"
        }
    }
}
```

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/fN5Qbmh0MPwmQ5J4W/builds/yoWunAV1OwQLEkOA7/openapi.json
