SKUFuse — Inventory Sync Guard
Pricing
from $500.00 / 1,000 comparison completeds
SKUFuse — Inventory Sync Guard
Compare current and proposed inventory CSV snapshots before synchronization. Flags large stock changes, stock going to zero, and SKUs added or removed. Comparison only
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
19 hours ago
Last modified
Categories
Share
SKUFuse compares a current inventory CSV snapshot with a proposed inventory CSV snapshot and reports every change that needs human review before you synchronize it — stock drops or increases beyond your thresholds, stock going to zero, SKUs added or removed, duplicate SKUs, and rows it could not check.
What it is not. SKUFuse never synchronizes anything. It does not connect
to an ERP, PIM, WMS, marketplace, Shopify, WooCommerce, or any other live
system. It does not update or reserve stock, repair files, forecast demand, or
infer live availability. A clean report (no_findings) means no review items
were found — it is not a guarantee that a synchronization 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 snapshots, turn Run synthetic demo off, then:
- Upload your current inventory CSV and your proposed snapshot CSV under Files — straight from your computer, up to 10 MB each.
- Actively check the complete-snapshots declaration.
- 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 (rules,
maxFindings) 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 snapshot
comparison. A demo run is billed like any other run (3 evaluated rows —
see Pricing).
The built-in pair: DEMO-COF-001 drops 12 → 0; DEMO-MUG-002 is unchanged;
DEMO-TEE-003 is added at stock 8.
Expected demo output
status: "review_required", syntheticDemo: true, evaluatedRows: 3,
addedRows: 1, and three findings: STOCK_DROP_EXCEEDS_THRESHOLD and
STOCK_BECOMES_ZERO on DEMO-COF-001 (12 → 0 vs. the default 50 % drop
threshold), plus SKU_ADDED on DEMO-TEE-003.
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.
evaluatedRows counts every deterministically classified proposed row: matched
pairs and valid additions. Not billable: removed SKUs (current-only 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
- One fixed CSV profile (
inventory_snapshot_single_location_v1):SKU+Stockcolumns, one location, one snapshot pair. Multi-location inventory, pricing, and other CSV dialects are out of scope. - Comparison only. No synchronization, no live-system connection, no
repair, no forecasting.
liveInventoryVerifiedandsyncSafetyGuaranteedare alwaysfalsein the report — by design. - Exact, case-sensitive SKU matching; duplicates are never resolved — every duplicate makes its SKU uncheckable.
- Additions and removals are only meaningful on complete snapshots. The
confirmCompleteSnapshotsdeclaration is your explicit assertion that both files cover the same complete inventory scope — without it the run refuses to interpret absence as an addition or removal. - 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 inventory 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 snapshots
(
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 inventory 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
Produce one current snapshot and one proposed snapshot of the same complete
inventory scope — the set of SKUs and on-hand quantities you are about to
synchronize. Fixed profile inventory_snapshot_single_location_v1:
- Required columns:
SKU,Stock— exact names, case-sensitive, order-insensitive; extra columns are ignored and reported underignoredColumns. - SKU — exact, case-sensitive matching; up to 256 UTF-8 bytes; leading zeroes, Unicode variants, and lookalike characters stay distinct; boundary whitespace is rejected, not trimmed.
- Stock — a signed base-10 integer string with an optional leading minus
and ASCII digits only (
-2147483648–2147483647). Plus signs, decimals, exponents, thousands separators, surrounding whitespace, empty cells, and status text likeinstockare all rejected. - Every data row is validated — there is no ignored-row convention in this profile. A row that fails validation is a finding, never silently skipped.
- Encoding: UTF-8 only; one leading BOM tolerated; NUL rejected; LF/CRLF/CR endings all accepted.
The complete-snapshots declaration — actively confirmed, never pre-filled
confirmCompleteSnapshots has no default or prefill anywhere: you must set it
to true yourself. It asserts that both files represent the same complete
inventory scope — only then is a missing SKU a genuine addition or removal
rather than a scope mismatch. Real comparisons fail
COMPLETE_SNAPSHOTS_UNCONFIRMED without it. It is not required for demoMode
— no caller files exist for it to describe.
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 inventory_snapshot_single_location_v1; also accepted (and validated) in demo mode |
confirmCompleteSnapshots | real comparisons | Must be true: you assert both snapshots cover the same complete inventory scope |
currentCsvFile / currentCsv / currentCsvUrl | real comparisons, one of them | Current snapshot — 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 snapshot, same rules |
rules.maxStockDropPercent | no, default 50 | 0–10000, step 0.01. Flagged when the decrease strictly exceeds it |
rules.maxStockIncreasePercent | no, default 200 | 0–10000, step 0.01. Same rule for increases |
rules.flagStockToZero | no, default true | Flag stock changing positive → 0 |
rules.flagAddedSku | no, default true | Flag SKUs present only in the proposed snapshot |
rules.flagRemovedSku | no, default true | Flag SKUs present only in the current snapshot |
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 drop-to-zero example
Current A100 at stock 12, proposed at 0 — a 100 % decrease. With the
default 50 % drop threshold the report returns status: "review_required"
with two findings, in this order:
STOCK_DROP_EXCEEDS_THRESHOLD— before12, after0,changePercent: "-100.0000", threshold50STOCK_BECOMES_ZERO— stock12 → 0
Percentage rules run only from a positive current baseline — a zero or negative current stock has no meaningful percentage, so no percentage finding is ever invented from it.
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 / added / removed /
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 |
liveInventoryVerified / syncSafetyGuaranteed | Always false — by design |
syntheticDemo | true only for the built-in synthetic demonstration — never for a real snapshot comparison |
rules, maxFindings | The normalized options the report was produced under |
currentRows, proposedRows | Data-record counts per file |
evaluatedRows | Proposed rows deterministically classified — matched pairs plus valid additions |
addedRows | Valid unique proposed SKUs genuinely absent from current |
removedRows | Valid unique current SKUs genuinely absent from proposed |
currentUncheckableRows | Defective current rows excluded from matching |
proposedUncheckableRows | Proposed rows that could not be evaluated or classified |
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 order |
Status priority: incomplete (any uncheckable row or truncation) →
no_changes (both files empty of data rows) → review_required
(findings or classified additions/removals) → no_findings.
Finding order: current-file row diagnostics in source order, then proposed-file findings in source order, then removal findings in current source order.
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 — never resolved to a winner |
INVALID_STOCK | warning | Stock cell fails the signed-integer grammar |
SKU_ADDED | review | Proposed SKU genuinely absent from the current snapshot |
SKU_REMOVED | review | Current SKU genuinely absent from the proposed snapshot |
CURRENT_SKU_AMBIGUOUS | uncheckable | Proposed SKU matches duplicated current SKUs |
CURRENT_ROW_UNCHECKABLE | uncheckable | The matched current row is defective |
STOCK_DROP_EXCEEDS_THRESHOLD | warning | Decrease strictly above the drop threshold |
STOCK_INCREASE_EXCEEDS_THRESHOLD | warning | Increase strictly above the increase threshold |
STOCK_BECOMES_ZERO | warning | 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). field names are the profile column names (SKU, Stock). 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-inventory-sync-guard').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,
COMPLETE_SNAPSHOTS_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.