SKUFuse — WooCommerce Catalog Update Review
Pricing
from $500.00 / 1,000 comparison completeds
SKUFuse — WooCommerce Catalog Update Review
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.
Pricing
from $500.00 / 1,000 comparison completeds
Rating
0.0
(0)
Developer
Lasse Bjerkholt Olsen
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
16 hours ago
Last modified
Categories
Share
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)
- Open the Actor in Apify Console and go to the Input tab.
- 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.
- 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.
- 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.
- Key-value store —
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:
{ "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).
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
maxTotalChargeUsdcap. If the whole report does not fit, it failsBUDGET_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.
storeStateVerifiedandimportSafetyGuaranteedare alwaysfalsein the report — by design. - Exact, case-sensitive SKU matching; duplicates are never resolved — every duplicate makes its SKU uncheckable.
currencyis 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: truecombined 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 underignoredColumns - Optional
Typecolumn — values other thansimpleare flagged (UNSUPPORTED_PRODUCT_TYPE). If either file has noTypecolumn,confirmSimpleProducts: trueis 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— before49.90, after4.99,changePercent: "-90.0000", threshold30STOCK_BECOMES_ZERO— stock12 → 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 (
identityencoding 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
# Start a runcurl -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 outputscurl "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
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 itemconst csv = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('FINDINGS.csv');
Make / n8n (plain HTTP)
- HTTP → Make a request:
POSThttps://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). - Or async:
POST …/runs→ pollGET …/runs/{runId}untilstatus: SUCCEEDED→GET …/datasets/{defaultDatasetId}/items. - On
FAILED, fetch…/key-value-stores/{storeId}/records/ERRORfor the stableerror.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)
npm cinpm run build# Seed a local inputmkdir -p storage/key_value_stores/defaultcat > 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"}JSONCRAWLEE_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 reportkey_value_stores/default/OUTPUT.json— the full comparison reportkey_value_stores/default/FINDINGS.csv— a spreadsheet-ready review sheetkey_value_stores/default/RUN_METADATA.json— operational/billing metadatadatasets/default/*.json— exactly one dataset item (the same report)
npm test # all suites (unit/contract/integration/e2e/performance)npm run test:unit # unit tests onlynpm run test:contract # schema + packaging contract testsnpm run test:e2e # compiled-Actor end-to-endnpm run test:coverage # coverage with per-file thresholdsnpm run typecheck && npm run lint && npm run format:checknpm 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):
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.