# SKUFuse — WooCommerce Catalog Update Review (`overbifrost/skufuse`) Actor

Compare current and proposed WooCommerce catalog CSVs before import. SKUFuse flags significant price changes, stock going to zero, unmatched SKUs, duplicates, and rows requiring manual review. Comparison only — it never writes to your store.

- **URL**: https://apify.com/overbifrost/skufuse.md
- **Developed by:** [Lasse Bjerkholt Olsen](https://apify.com/overbifrost) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $500.00 / 1,000 comparison completeds

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

## SKUFuse — Catalog Update Review

SKUFuse compares a **current WooCommerce catalog CSV** with a **proposed update
CSV** and reports every change that needs human review **before** you import it
— price drops or increases beyond your thresholds, stock going to zero,
unmatched SKUs, duplicate SKUs, and rows it could not check.

**What it is not.** SKUFuse never imports anything. It does not connect to a
store, write to a store, repair files, or verify live state. A clean report
(`no_findings`) means *no review items were found* — it is not a guarantee that
an import is safe.

### Quick start (Apify Console)

1. Open the Actor in Apify Console and go to the **Input** tab.
2. Leave **Run synthetic demo** turned **on** and press **Start** — SKUFuse
   compares its built-in DEMO-\* data and you immediately see what a report
   looks like. No files needed.
3. For your real catalog, turn **Run synthetic demo** **off**, then:
   - **Upload** your current catalog CSV and your proposed update CSV under
     **Files** — straight from your computer, up to 10 MB each.
   - Set your **Currency** and actively check the declarations.
4. Press **Start**, then open **Review report (HTML)** in the run's output —
   a readable page with the status, the counts that matter, and the findings.
   The canonical report and review sheet are also in storage:
   - **Key-value store** — `REVIEW.html` (the readable report), `OUTPUT`
     (canonical JSON report), `FINDINGS.csv` (spreadsheet-ready review sheet),
     `RUN_METADATA` (operational/billing metadata).
   - **Dataset** — exactly one item: the same JSON report.

Pasting CSV text or pointing to a public HTTPS URL works too — those fields
live under **Advanced** and are meant for API use and automation. Each file
still needs exactly one source: upload, paste, or URL.

### Synthetic example input

The demonstration mode exists so you can evaluate the output before preparing
exports — and it is what the platform's automated health check runs. It
compares a fixed, built-in synthetic pair and nothing else:

```json
{ "demoMode": true }
```

An entirely empty input object `{}` selects the same demonstration — that is
the input a bare default-input run produces. Every other option
(`currency`, `rules`, `maxFindings`, the declarations) is honored exactly as in
a real run when supplied.

**The demonstration never mixes with your data.** `demoMode: true` combined
with any source field (`currentCsvFile`, `currentCsv`, `currentCsvUrl`,
`proposedCsvFile`, `proposedCsv`, `proposedCsvUrl`) fails `INPUT_INVALID`.
Reports from it are marked
`syntheticDemo: true` so they can never be mistaken for a real catalog
comparison. A demo run is billed like any other run (3 evaluated rows —
see [Pricing](#pricing-pay-per-event)).

The built-in pair: `DEMO-COF-001` drops `49.90 → 4.99` (~90 %) with stock
`12 → 0`; `DEMO-MUG-002` is unchanged; `DEMO-TEE-003` rises `25.00 → 27.50`
(+10 %, under the default 30 % increase threshold).

#### Expected demo output

`status: "review_required"`, `syntheticDemo: true`, `evaluatedRows: 3`,
`coverageComplete: true`, and two findings on `DEMO-COF-001`:
`PRICE_DROP_EXCEEDS_THRESHOLD` (`-90.0000 %` vs. threshold `30`) and
`STOCK_BECOMES_ZERO` (`12 → 0`).

### Pricing (pay-per-event)

| Event | Price | Units charged |
|---|---|---|
| `comparison-completed` | **$0.50** | 1 if `evaluatedRows > 0`, else 0 |
| `row-evaluated` | **$0.00025** | `evaluatedRows` |
| `apify-actor-start` | **$0.00005** | 1 per run — platform-managed, always applies |

Charged **only after** all report outputs are delivered. A 3-row demonstration
run costs $0.50080; a 1,000-row comparison costs $0.75005.

`apify-actor-start` is charged automatically once per run at the configured
memory (≤1 GiB) before any Actor code runs. It is a platform charge, not a
SKUFuse custom event: the Actor never calls `charge()` for it, it never appears
in `RUN_METADATA.billing.expected`/`confirmed`, and it applies on **every**
pay-per-event run — including zero-custom-charge outcomes like header-only,
invalid-input, or insufficient-budget runs. `apify-default-dataset-item` and
any other paid synthetic event are not permitted.

Not billable by custom events: unmatched rows, uncheckable rows, header-only
files, and insufficient-budget runs. An unchanged evaluated row is billable;
a row with several findings is still one row event.

Before charging, the run checks the **combined** cost (the platform start fee

- base + all row events) against the run's budget cap with exact decimal
  arithmetic — the start fee is inside the same `maxTotalChargeUsd` cap. If the
  whole report does not fit, it fails `BUDGET_INSUFFICIENT` — no report, no
  partial custom charges, no billed prefix. Effective prices come from the live
  run's pricing info, never hardcoded.

Local runs (`billing.mode: local`) and unconfigured private Actors
(`unconfigured`) never charge (`billing.outcome: not_applicable`).

#### Failed runs and confirmed charges

Charging is the last step, so a run can fail *after* one or more charges were
already confirmed. A failed run is **not** automatically non-billable — the
billing outcome is a separate fact recorded in `RUN_METADATA.billing.outcome`:

| Outcome | Meaning |
|---|---|
| `blocked` | Nothing was attempted or confirmed — e.g. the run failed before charging began. Not billable. |
| `completed` | Every expected unit was confirmed — even if the run then failed during final bookkeeping. The charge stands. |
| `partial` | Some units were confirmed but charging did not complete — the confirmed units stand. |
| `unknown` | A charge's result could not be confirmed. Uncertain is **not** zero; the Actor never retries an uncertain charge. |
| `not_required` | Pay-per-event run with zero evaluated rows — no custom units due (the platform's `apify-actor-start` fee still applies). |
| `not_applicable` | Local or unconfigured run — charging never ran. |
| `pending` | Transient — a run killed before final bookkeeping may leave this in the last persisted metadata; it is not a settled outcome. |

Report outputs already delivered (`OUTPUT`, `FINDINGS.csv`, `REVIEW.html`, the dataset item)
stay in the run's storage whatever the outcome. The exact expected and
confirmed unit counts per event are in `RUN_METADATA.billing.expected` and
`RUN_METADATA.billing.confirmed`; the platform-side charge record is in Apify
Console under the run's usage/charges.

There is no all-or-nothing charging, no automatic refund, and no exactly-once
guarantee: a confirmed charge is not undone by a later failure, and each new
run is a new billable comparison.

### Limitations

- **WooCommerce simple products only**, one fixed CSV profile
  (`woo_simple_price_stock_v1`). Other platforms, product types, and CSV
  dialects are out of scope.
- **Comparison only.** No import, no store connection, no live-state check,
  no repair. `storeStateVerified` and `importSafetyGuaranteed` are always
  `false` in the report — by design.
- Exact, case-sensitive SKU matching; duplicates are never resolved — every
  duplicate makes its SKU uncheckable.
- `currency` is a declared label for report context — the Actor does not
  convert prices or verify it against a store.
- Hard ceilings below; exceeding them fails the run with a stable error code —
  source data is never silently truncated.

| Resource | Ceiling |
|---|---|
| Whole input object | 2 MiB |
| Each inline CSV | 512 KiB |
| Each remote CSV | 10 MiB streamed (Content-Length is not trusted) |
| Data records per file | 100,000 |
| Columns per file | 128 |
| Retained findings | `maxFindings`, hard cap 1000 |
| Source URL | HTTPS only, port 443, ≤ 4096 bytes |
| Remote retrieval | 30 s total (DNS 5 s, connect/TLS 10 s, idle 10 s) |
| Whole run | 100 s application deadline |

### Privacy and retention

- The demonstration mode sends **no catalog data anywhere** — it compares
  built-in DEMO-\* rows only, and `demoMode: true` combined with any source
  field is rejected.
- Apify retains the run's **INPUT** record under the Actor's storage and
  retention settings. Input may contain complete inline catalogs
  (`currentCsv`/`proposedCsv`) or source URLs — including signed URLs with
  embedded credentials. Prefer pre-signed URLs with short expiry, or inline
  CSVs you are comfortable retaining, and clean up run storage when done.
- The report echoes catalog content — that is the product. It stays in your
  run's storage under Apify's normal storage retention rules.
- `ERROR`, `RUN_METADATA`, `RUN_STATE`, and log output contain **no** URLs,
  SKUs, cell values, secrets, or raw exception text. Input validation errors
  point at a sanitized location (a field name for supported fields, a fixed
  marker for unsupported ones) — never the offending property name verbatim.
- No analytics, no telemetry, no cross-run stores, no external services beyond
  your two declared source URLs.
- Re-running the Actor in a used store fails `RESUME_UNSUPPORTED` — a run
  cannot silently append to or re-charge an earlier run's evidence.

### Running a real comparison

#### Prepare the two CSVs

Fixed WooCommerce profile `woo_simple_price_stock_v1`:

- Required columns: **`SKU`, `Regular price`, `Stock`** — order-insensitive,
  extra columns are ignored and reported under `ignoredColumns`
- Optional **`Type`** column — values other than `simple` are flagged
  (`UNSUPPORTED_PRODUCT_TYPE`). If either file has no `Type` column,
  `confirmSimpleProducts: true` is required
- **SKU** — exact, case-sensitive matching; up to 256 UTF-8 bytes; boundary
  whitespace is rejected, not trimmed
- **Regular price** — 1–9 integer digits plus 0–6 decimals (e.g. `49.90`)
- **Stock** — 1–10 digits, `0`–`2147483647`
- Encoding: UTF-8 only; one leading BOM tolerated; NUL rejected; LF/CRLF/CR
  endings all accepted

#### Declarations — actively confirmed, never pre-filled

`confirmComparablePrices` has no default or prefill anywhere: you must set it
to `true` yourself, asserting that both files share currency, tax basis, and
regular-price meaning. Real comparisons without it fail
`PRICE_CONTEXT_UNCONFIRMED`. It is not required for `demoMode` — no caller
files exist for it to describe — and no `demoMode` default can silently
authorize a customer comparison.

#### Input contract

One JSON object. Unknown properties, nulls, wrong types, and unsupported
options are rejected — nothing is coerced or ignored silently.

| Field | Required | Meaning |
|---|---|---|
| `demoMode` | no | `true` (or an empty input object) selects the built-in synthetic demonstration. Forbidden alongside any source field. |
| `schemaVersion` | real comparisons | Must be exactly `woo_simple_price_stock_v1`; also accepted (and validated) in demo mode |
| `currency` | real comparisons | Three-letter code `^[A-Z]{3}$`, e.g. `EUR` — a declared label only; no conversion or live lookup. Demo defaults to `EUR` |
| `confirmComparablePrices` | real comparisons | Must be `true`: you assert both files share currency, tax basis, and regular-price meaning |
| `confirmSimpleProducts` | no, default `false` | Must be `true` when either file lacks a `Type` column |
| `currentCsvFile` / `currentCsv` / `currentCsvUrl` | real comparisons, one of them | Current catalog — file upload, inline text, or HTTPS URL — never more than one, never none, never in demo mode |
| `proposedCsvFile` / `proposedCsv` / `proposedCsvUrl` | real comparisons, one of them | Proposed update, same rules |
| `rules.maxPriceDropPercent` | no, default `30` | 0–100, step 0.01. Flagged when the decrease **strictly exceeds** it |
| `rules.maxPriceIncreasePercent` | no, default `30` | 0–10000, step 0.01. Same rule for increases |
| `rules.flagStockToZero` | no, default `true` | Flag numeric stock changing positive → 0 |
| `maxFindings` | no, default `1000` | 1–1000. Bounds the returned finding list only — every finding is still counted |

Mixed modes are allowed (e.g. current uploaded + proposed inline, or current
inline + proposed via URL). `currentCsvFile`/`proposedCsvFile` take the
reference the Apify `fileupload` editor produces — an
`https://api.apify.com/v2/key-value-stores/{storeId}/records/{key}` URL.
Uploads are fetched from that key-value store record, capped at 10 MiB, and
validated byte-for-byte like every other source; any other URL shape is
rejected as `INPUT_INVALID`.

#### The 90 % drop example

Current `A100` at `49.90`, proposed at `4.99` — a ~90 % decrease, plus stock
`12 → 0`. With the default 30 % drop threshold the report returns
`status: "review_required"` with two findings:

- `PRICE_DROP_EXCEEDS_THRESHOLD` — before `49.90`, after `4.99`,
  `changePercent: "-90.0000"`, threshold `30`
- `STOCK_BECOMES_ZERO` — stock `12 → 0`

### Output

#### `REVIEW.html` (human-readable review report)

A self-contained HTML page generated from the canonical report: status
headline, the counts that drive a decision (evaluated / unmatched /
uncheckable rows, findings), scope and truncation notices, and the retained
findings in readable form. It carries no scripts or external assets and is
safe to open anywhere. `REVIEW.html` is a convenience presentation — `OUTPUT`
remains the canonical, machine-readable report.

#### `OUTPUT` (JSON report, also the single dataset item)

| Field | Meaning |
|---|---|
| `status` | `no_findings` · `review_required` · `incomplete` · `no_changes` |
| `scope` | Always `comparison_only` |
| `storeStateVerified` / `importSafetyGuaranteed` | Always `false` — by design |
| `syntheticDemo` | `true` only for the built-in synthetic demonstration — never for a real catalog comparison |
| `currency`, `rules`, `maxFindings` | The normalized options the report was produced under |
| `currentRows`, `proposedRows` | Data-record counts |
| `evaluatedRows` | Rows actually compared (exact SKU match + valid values) |
| `unmatchedRows` | Proposed rows without a current match |
| `uncheckableRows` | Matched rows that could not be evaluated |
| `currentUncheckableRows` | Defective current rows excluded from matching |
| `coverageComplete` | True only when proposed has rows, every proposed row was evaluated, and `currentUncheckableRows` is zero |
| `findingsTotal` / `findingsReturned` | All findings vs. the bounded list returned |
| `truncated` | True exactly when `findingsTotal > findingsReturned` |
| `findingsByCode` | Complete counts per code (zero counts omitted) |
| `ignoredColumns` | Ignored extra columns per file |
| `findings` | Bounded list, deterministic source order |

Status priority: **`incomplete`** (any uncheckable/unmatched row or
truncation) → **`no_changes`** (empty proposed file) →
**`review_required`** (findings) → **`no_findings`**.

#### Finding codes

| Code | Severity | Meaning |
|---|---|---|
| `MISSING_SKU`, `INVALID_SKU` | warning | SKU cell empty or invalid |
| `DUPLICATE_SKU` | warning | SKU appears more than once in a file |
| `UNSUPPORTED_PRODUCT_TYPE` | warning | `Type` is present but not `simple` |
| `INVALID_PRICE`, `INVALID_STOCK` | warning | Value cell fails the grammar |
| `UNMATCHED_SKU` | unmatched | Proposed SKU has no current counterpart |
| `CURRENT_SKU_AMBIGUOUS` | uncheckable | Proposed SKU matches duplicated current SKUs |
| `CURRENT_ROW_UNCHECKABLE` | uncheckable | The matched current row is defective |
| `ZERO_BASE_PRICE` | uncheckable | Current price is 0 — no percentage computable |
| `PRICE_DROP_EXCEEDS_THRESHOLD` | warning | Decrease strictly above the drop threshold |
| `PRICE_INCREASE_EXCEEDS_THRESHOLD` | warning | Increase strictly above the increase threshold |
| `STOCK_BECOMES_ZERO` | warning | Numeric stock positive → 0 |

Each finding carries `code`, `severity`, `source`, `row`, `line`, `sku`,
`field`, `message` — plus code-specific `before`/`after`/`changePercent`,
`currentRow`/`currentLine`, or a bounded `rawValue` excerpt (max 128 code
points). Findings never contain full rows, URLs, or raw exception text.

#### `FINDINGS.csv`

Same findings as a review spreadsheet — fixed columns, UTF-8 without BOM,
quoted as needed, formula-injection protected (cells starting with
`= + - @` or tab/CR/LF, ignoring leading spaces, get a `'` prefix).

#### `RUN_METADATA` / `RUN_STATE` / `ERROR`

Operational records: run state, `reportAvailable`, billing
mode/outcome/expected/confirmed counts, `errorCode`, `durationMs`. `ERROR`
records carry only a stable code, a safe message template, and structured
location fields — never URLs, SKUs, cell values, or raw exceptions.

### Remote CSV security

`currentCsvUrl`/`proposedCsvUrl` are deliberately constrained:

- HTTPS only, hostname required, port omitted or 443, no credentials in the
  URL, no fragment, no IP literals, no single-label hosts, no IPv6
- DNS must resolve to **public IPv4** only — every private/reserved/special
  range is blocked, including rebinding tricks and non-canonical IPv4 forms
- The connection is pinned to the approved DNS answer and the connected peer
  is re-verified — DNS pinning, not just DNS checking
- No redirects, no compression (`identity` encoding only)
- Actual streamed bytes capped at 10 MiB regardless of headers
- Deadlines: DNS 5 s, connect 10 s, idle 10 s, total 30 s

If your file is behind auth, download it yourself and use the inline CSV
fields — there are no credential or custom-header options by design.

### Using the API / SDK

#### REST

```bash
## Start a run
curl -X POST \
  "https://api.apify.com/v2/acts/YOUR_ACTOR_ID/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d @input.json

## Poll until SUCCEEDED, then read outputs
curl "https://api.apify.com/v2/key-value-stores/STORE_ID/records/OUTPUT?token=$APIFY_TOKEN"
curl "https://api.apify.com/v2/datasets/DATASET_ID/items?token=$APIFY_TOKEN"
```

#### JavaScript SDK

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('YOUR_USER/skufuse').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const report = items[0];              // the full report — exactly one item
const csv = await client.keyValueStore(run.defaultKeyValueStoreId)
  .getRecord('FINDINGS.csv');
```

#### Make / n8n (plain HTTP)

1. **HTTP → Make a request**: `POST`
   `https://api.apify.com/v2/acts/YOUR_ACTOR_ID/run-sync-get-dataset-items?token=…`
   with the input JSON — returns the dataset items directly when the run
   finishes (sync endpoint, simplest for automations).
2. Or async: `POST …/runs` → poll `GET …/runs/{runId}` until
   `status: SUCCEEDED` → `GET …/datasets/{defaultDatasetId}/items`.
3. On `FAILED`, fetch `…/key-value-stores/{storeId}/records/ERROR` for the
   stable `error.code`.

### Error codes

Fatal errors fail the run with a stable code (also in the `ERROR` record):

`INPUT_INVALID`, `INPUT_TOO_LARGE`, `UNSUPPORTED_SCHEMA`,
`PRICE_CONTEXT_UNCONFIRMED`, `SIMPLE_PRODUCTS_UNCONFIRMED`,
`SOURCE_URL_INVALID`, `SOURCE_DESTINATION_BLOCKED`, `SOURCE_DNS_FAILED`,
`SOURCE_FETCH_FAILED`, `SOURCE_TIMEOUT`, `SOURCE_REDIRECT_UNSUPPORTED`,
`SOURCE_HTTP_STATUS`, `SOURCE_ENCODING_UNSUPPORTED`, `SOURCE_TOO_LARGE`,
`CSV_INVALID_ENCODING`, `CSV_FORBIDDEN_CHARACTER`, `CSV_SYNTAX_INVALID`,
`CSV_HEADER_MISSING`, `CSV_HEADER_INVALID`, `CSV_DUPLICATE_HEADER`,
`CSV_MISSING_COLUMN`, `CSV_COLUMN_COUNT`, `CSV_LIMIT_EXCEEDED`,
`BUDGET_INSUFFICIENT`, `BILLING_CONFIGURATION_INVALID`,
`BILLING_STATE_CONFLICT`, `BILLING_PARTIAL`, `BILLING_OUTCOME_UNKNOWN`,
`OUTPUT_WRITE_FAILED`, `RESUME_UNSUPPORTED`, `RUN_DEADLINE_EXCEEDED`,
`INTERNAL_ERROR`.

### Development (local, no Apify account)

```bash
npm ci
npm run build

## Seed a local input
mkdir -p storage/key_value_stores/default
cat > storage/key_value_stores/default/INPUT.json <<'JSON'
{
  "schemaVersion": "woo_simple_price_stock_v1",
  "currency": "EUR",
  "confirmComparablePrices": true,
  "confirmSimpleProducts": true,
  "currentCsv": "SKU,Regular price,Stock\nA100,49.90,12",
  "proposedCsv": "SKU,Regular price,Stock\nA100,4.99,0"
}
JSON

CRAWLEE_STORAGE_DIR=./storage node dist/main.js
```

An empty `INPUT.json` object (`{}`) runs the synthetic demonstration locally —
same as `demoMode: true`.

Results land in `storage/`:

- `key_value_stores/default/REVIEW.html` — the human-readable review report
- `key_value_stores/default/OUTPUT.json` — the full comparison report
- `key_value_stores/default/FINDINGS.csv` — a spreadsheet-ready review sheet
- `key_value_stores/default/RUN_METADATA.json` — operational/billing metadata
- `datasets/default/*.json` — exactly one dataset item (the same report)

```bash
npm test              # all suites (unit/contract/integration/e2e/performance)
npm run test:unit     # unit tests only
npm run test:contract # schema + packaging contract tests
npm run test:e2e      # compiled-Actor end-to-end
npm run test:coverage # coverage with per-file thresholds
npm run typecheck && npm run lint && npm run format:check
npm run test:cloud    # authorized Apify cloud verification (needs APIFY_TOKEN
                      # + APIFY_ACTOR_ID + APIFY_BUILD; every PPE run — all
                      # scenarios — needs APIFY_AUTHORIZE_CHARGES=1 +
                      # APIFY_MAX_CHARGE_USD since the platform start fee
                      # applies to each run)
```

Container (when Docker is available):

```bash
docker build -t skufuse .
bash scripts/container-check.sh skufuse:latest
```

The deterministic comparison core (`src/compare.ts`, `src/csv.ts`, `src/rows.ts`,
`src/money.ts`, `src/report.ts`, `src/findingsCsv.ts`) has zero Apify imports —
it is fully testable without the platform.

# Actor input Schema

## `demoMode` (type: `boolean`):

Try SKUFuse on built-in synthetic data — no files needed and nothing about your catalog is sent. Leave ON for a first look at the report; turn OFF to compare your own WooCommerce CSVs. When off, real sources and the declarations below are required. A demo run is billed like any other run (one comparison over 3 rows).

## `currentCsvFile` (type: `string`):

Upload the current catalog export as a CSV file from your computer (up to 10 MB). Exactly one source per file — either this upload or one of the advanced options below.

## `proposedCsvFile` (type: `string`):

Upload the proposed update CSV from your computer (up to 10 MB) — the file you plan to import. Exactly one source per file.

## `rules` (type: `object`):

Configure the price-change thresholds and whether stock changes from positive to zero are reported.

## `currency` (type: `string`):

ISO 4217 code for the compared prices, e.g. EUR or USD. Required for a real comparison. Declared context only — the Actor does not convert prices.

## `confirmComparablePrices` (type: `boolean`):

Required declaration: both files use the same currency and the same price basis (e.g. both net or both gross). Intentionally not pre-checked.

## `confirmSimpleProducts` (type: `boolean`):

Only needed when either CSV lacks the Type column. Check this to assert that rows in files without Type are simple products. A present Type value still must be exactly simple; this does not override variations, grouped, or external products.

## `currentCsv` (type: `string`):

Paste the current catalog export as CSV text (max 512 KB). An alternative to the file upload above — use exactly one source per file.

## `proposedCsv` (type: `string`):

Paste the proposed update CSV as text (max 512 KB). Exactly one source per file.

## `currentCsvUrl` (type: `string`):

Public HTTPS URL returning the current CSV (max 10 MB). No redirects, no URL credentials, port 443 only.

## `proposedCsvUrl` (type: `string`):

Public HTTPS URL returning the proposed CSV (max 10 MB). No redirects, no URL credentials, port 443 only.

## `maxFindings` (type: `integer`):

Maximum number of findings retained in the report (1–1000). Totals are always counted completely; only the returned list is bounded.

## `schemaVersion` (type: `string`):

Fixed input contract version. Any other value fails with UNSUPPORTED\_SCHEMA.

## Actor input object example

```json
{
  "demoMode": true,
  "currency": "EUR",
  "confirmSimpleProducts": false,
  "currentCsvUrl": "https://example.com/exports/current.csv",
  "proposedCsvUrl": "https://example.com/exports/proposed.csv",
  "maxFindings": 1000,
  "schemaVersion": "woo_simple_price_stock_v1"
}
```

# Actor output Schema

## `reviewHtml` (type: `string`):

No description

## `findingsCsv` (type: `string`):

No description

## `report` (type: `string`):

No description

## `datasetItem` (type: `string`):

No description

## `runMetadata` (type: `string`):

No description

## `errorRecord` (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 = {
    "demoMode": true,
    "schemaVersion": "woo_simple_price_stock_v1"
};

// Run the Actor and wait for it to finish
const run = await client.actor("overbifrost/skufuse").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 = {
    "demoMode": True,
    "schemaVersion": "woo_simple_price_stock_v1",
}

# Run the Actor and wait for it to finish
run = client.actor("overbifrost/skufuse").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 '{
  "demoMode": true,
  "schemaVersion": "woo_simple_price_stock_v1"
}' |
apify call overbifrost/skufuse --silent --output-dataset

```

## MCP server setup

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

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/r4hgNENY5bFRdbiAk/builds/gHmVldwGIJrHdF4ui/openapi.json
