SKUFuse — Shopify Catalog Update Review avatar

SKUFuse — Shopify Catalog Update Review

Pricing

from $500.00 / 1,000 comparison completeds

Go to Apify Store
SKUFuse — Shopify Catalog Update Review

SKUFuse — Shopify Catalog Update Review

Compare current and proposed Shopify product CSVs before import. SKUFuse flags significant price changes, stock going to zero, unmatched or ambiguous SKUs, 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

Lasse Bjerkholt Olsen

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

15 hours ago

Last modified

Categories

Share

SKUFuse compares a current Shopify product 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, inventory 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 products 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:

{ "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 inventory 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)

EventPriceUnits charged
comparison-completed$0.501 if evaluatedRows > 0, else 0
row-evaluated$0.00025evaluatedRows
apify-actor-start$0.000051 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, ignored image/metadata-only 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:

OutcomeMeaning
blockedNothing was attempted or confirmed — e.g. the run failed before charging began. Not billable.
completedEvery expected unit was confirmed — even if the run then failed during final bookkeeping. The charge stands.
partialSome units were confirmed but charging did not complete — the confirmed units stand.
unknownA charge's result could not be confirmed. Uncertain is not zero; the Actor never retries an uncertain charge.
not_requiredPay-per-event run with zero evaluated rows — no custom units due (the platform's apify-actor-start fee still applies).
not_applicableLocal or unconfigured run — charging never ran.
pendingTransient — 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

  • Shopify product CSV, single-location inventory only, one fixed profile (shopify_product_price_stock_single_location_v1). Shopify's product CSV documents Inventory quantity as the stock of a single location — there is no multi-location quantity in this format, so multi-location inventory is out of scope. 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.
ResourceCeiling
Whole input object2 MiB
Each inline CSV512 KiB
Each remote CSV10 MiB streamed (Content-Length is not trusted)
Data records per file100,000
Columns per file128
Retained findingsmaxFindings, hard cap 1000
Source URLHTTPS only, port 443, ≤ 4096 bytes
Remote retrieval30 s total (DNS 5 s, connect/TLS 10 s, idle 10 s)
Whole run100 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

Export products from Shopify admin (Products → Export) for both the current and proposed files. Fixed Shopify profile shopify_product_price_stock_single_location_v1:

  • Required columns: SKU, Price, Inventory tracker, Inventory quantity — exact current native names, case-sensitive, order-insensitive; extra columns (e.g. URL handle, Title,
    Option1 name
    , image columns) are ignored and reported under ignoredColumns. Legacy Variant SKU/Variant Price-style headers are not aliases — a legacy-format file fails CSV_MISSING_COLUMN.
  • SKU — exact, case-sensitive matching; up to 256 UTF-8 bytes; boundary whitespace is rejected, not trimmed
  • Price — 1–9 integer digits plus 0–6 decimals (e.g. 49.90)
  • Inventory tracker — only shopify makes the row's quantity comparable. Blank, deny, shipwire, amazon_marketplace_web, or any other value is flagged UNSUPPORTED_INVENTORY_TRACKER and the row's quantity is not evaluated
  • Inventory quantity — signed 32-bit integer (-2147483648–2147483647); negative values are valid Shopify data, 0 transitions are flagged
  • Image/metadata-only rows are ignored, not diagnosed. Shopify CSVs interleave product, variant, and continuation rows under a repeated URL handle. A data record that carries no comparison signal — no SKU, Price, Title, option, or other product/variant attribute, i.e. the handle-plus-image-URL rows Shopify emits for extra product images — is skipped entirely: never matched, never billed, never a finding. The counts are disclosed in the report's ignoredRows.current/ignoredRows.proposed.
  • Encoding: UTF-8 only; one leading BOM tolerated; NUL rejected; LF/CRLF/CR endings all accepted

Declarations — actively confirmed, never pre-filled

confirmComparablePrices and confirmSingleLocationInventory have no default or prefill anywhere: you must set both to true yourself. The first asserts both files share currency, tax basis, and price meaning. The second asserts you understand Shopify's product CSV exposes single-location inventory only — comparing multi-location semantics through this profile is unsupported. Real comparisons fail PRICE_CONTEXT_UNCONFIRMED / SINGLE_LOCATION_UNCONFIRMED without them. Neither is required for demoMode — no caller files exist for them to describe.

Input contract

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

FieldRequiredMeaning
demoModenotrue (or an empty input object) selects the built-in synthetic demonstration. Forbidden alongside any source field.
schemaVersionreal comparisonsMust be exactly shopify_product_price_stock_single_location_v1; also accepted (and validated) in demo mode
currencyreal comparisonsThree-letter code ^[A-Z]{3}$, e.g. USD — a declared label only; no conversion or live lookup. Demo defaults to EUR
confirmComparablePricesreal comparisonsMust be true: you assert both files share currency, tax basis, and price meaning
confirmSingleLocationInventoryreal comparisonsMust be true: you assert the comparison is single-location inventory only
currentCsvFile / currentCsv / currentCsvUrlreal comparisons, one of themCurrent catalog — file upload, inline text, or HTTPS URL — never more than one, never none, never in demo mode
proposedCsvFile / proposedCsv / proposedCsvUrlreal comparisons, one of themProposed update, same rules
rules.maxPriceDropPercentno, default 300–100, step 0.01. Flagged when the decrease strictly exceeds it
rules.maxPriceIncreasePercentno, default 300–10000, step 0.01. Same rule for increases
rules.flagStockToZerono, default trueFlag Inventory tracker: shopify quantity changing positive → 0
maxFindingsno, default 10001–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 inventory 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 — quantity 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 / ignored 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)

FieldMeaning
statusno_findings · review_required · incomplete · no_changes
scopeAlways comparison_only
storeStateVerified / importSafetyGuaranteedAlways false — by design
syntheticDemotrue only for the built-in synthetic demonstration — never for a real catalog comparison
currency, rules, maxFindingsThe normalized options the report was produced under
currentRows, proposedRowsComparison-relevant data-record counts (image/metadata-only rows excluded)
ignoredRows.current / ignoredRows.proposedImage/metadata-only rows skipped per file
evaluatedRowsRows actually compared (exact SKU match + valid values)
unmatchedRowsProposed rows without a current match
uncheckableRowsMatched rows that could not be evaluated
currentUncheckableRowsDefective current rows excluded from matching
coverageCompleteTrue only when proposed has comparison rows, every proposed comparison row was evaluated, and currentUncheckableRows is zero
findingsTotal / findingsReturnedAll findings vs. the bounded list returned
truncatedTrue exactly when findingsTotal > findingsReturned
findingsByCodeComplete counts per code (zero counts omitted)
ignoredColumnsIgnored extra columns per file
findingsBounded 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

CodeSeverityMeaning
MISSING_SKU, INVALID_SKUwarningSKU cell empty or invalid
DUPLICATE_SKUwarningSKU appears more than once in a file
UNSUPPORTED_INVENTORY_TRACKERwarningInventory tracker is not shopify — quantity not comparable
INVALID_PRICE, INVALID_STOCKwarningValue cell fails the grammar
UNMATCHED_SKUunmatchedProposed SKU has no current counterpart
CURRENT_SKU_AMBIGUOUSuncheckableProposed SKU matches duplicated current SKUs
CURRENT_ROW_UNCHECKABLEuncheckableThe matched current row is defective
ZERO_BASE_PRICEuncheckableCurrent price is 0 — no percentage computable
PRICE_DROP_EXCEEDS_THRESHOLDwarningDecrease strictly above the drop threshold
PRICE_INCREASE_EXCEEDS_THRESHOLDwarningIncrease strictly above the increase threshold
STOCK_BECOMES_ZEROwarningTracked quantity 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). field names are the Shopify column names (SKU, Price, Inventory tracker, Inventory quantity). 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

# 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

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('YOUR_USER/skufuse-shopify').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, SINGLE_LOCATION_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 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": "shopify_product_price_stock_single_location_v1",
"currency": "USD",
"confirmComparablePrices": true,
"confirmSingleLocationInventory": true,
"currentCsv": "URL handle,SKU,Price,Inventory tracker,Inventory quantity\na100,A100,49.90,shopify,12",
"proposedCsv": "URL handle,SKU,Price,Inventory tracker,Inventory quantity\na100,A100,4.99,shopify,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)
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):

docker build -f actors/shopify/Dockerfile -t skufuse-shopify .
bash actors/shopify/scripts/container-check.sh skufuse-shopify: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.