Shopify Product CSV Validator & Cleaner avatar

Shopify Product CSV Validator & Cleaner

Pricing

from $1.00 / 1,000 variant checkeds

Go to Apify Store
Shopify Product CSV Validator & Cleaner

Shopify Product CSV Validator & Cleaner

Checks your Shopify product CSV before import: each variant's errors and warnings (columns, options, SKUs, GTIN check digits, prices, image URLs), one row per issue, a summary and a cleaned CSV; failed and unchanged rows are never charged. Not affiliated with or endorsed by Shopify.

Pricing

from $1.00 / 1,000 variant checkeds

Rating

0.0

(0)

Developer

COMPASS DEV

COMPASS DEV

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

18 days ago

Last modified

Share

Checks your Shopify product CSV before you import it and returns every variant's verdict with its errors and warnings, one row per issue and a summary, plus a cleaned CSV with the safe fixes; failed and unchanged rows are never charged.

Paste your product CSV (or pick the key-value store that holds it) and see, line by line, what Shopify's import would refuse or misread: unknown or misspelt columns, missing titles, bad URL handles, broken variant options, duplicate SKUs and barcodes, GTIN/EAN/UPC check digits, prices with currency symbols or decimal commas, compare-at prices below the price, invalid image URLs, wrong true/false and status values, weights with units. Built for Shopify merchants and agencies, dropshippers with supplier feeds, migrations from other platforms, and AI agents that prepare listings. Not affiliated with or endorsed by Shopify.

What it does

  • Checks the whole file against Shopify's published product CSV format: the current column names (URL handle, Description, SKU, Barcodes, Option1 name …) and the older ones Shopify still accepts (Handle, Body (HTML), Variant SKU, Variant Price, Image Src …), market, Google Shopping and metafield columns.
  • One row per variant with its verdict (ready, warnings or errors), a qualityScore, its issue codes and each issue's line, column, message and suggested value. A product's own issues (title, handle, images, tags) are listed on its first variant.
  • One free row per issue (rowType: "issue"), handy in a spreadsheet, and one free summary row per file: counts, importReady, feedQualityScore, the issue counts and the unknown columns.
  • A cleaned CSV (CLEANED_CSV in the run's key-value store): your file with only the safe fixes applied (trimmed cells, column name letter case, Yes → true, Active → active, $29.99 → 29.99, 1,299.00 → 1299.00, 10.0 → 10, UTF-8 with LF line ends). It never invents or changes a fact: no title, brand, barcode, price, weight, handle or image is ever made up or changed; those get a suggestion only.
  • Watch mode (give a watch list name in stateName): upload each new version of your feed and get only the variants that are new or whose issues changed, plus a price_jump warning when a price moved by priceJumpPercent or more. A watch run with nothing new returns exactly one free no_data row that says so.
  • Nothing is fetched: image URLs are checked as text only; the actor sends no request to Shopify or to your images.
  • You are never charged for failed results: failed, no_data, issue, summary and unchanged rows are free.

Quick start

Paste your CSV (header line first) in Product CSV (text), or as JSON:

{ "csvText": "URL handle,Title,Option1 name,Option1 value,SKU,Price\nmug,Stoneware Mug,Title,Default Title,MUG-350,12.50\n", "currency": "USD" }

A large file in your own key-value store (upload it with the Apify API or another Actor), checked every week in watch mode:

{ "keyValueStoreId": "aBcDeFgHiJkLmNoPq", "recordKey": "products.csv", "stateName": "my-store", "currency": "EUR", "decimalComma": true }

Input

FieldTypeDefaultDescription
csvTexttext— (prefilled with a small sample)Your Shopify product CSV, header line first; up to about 9 MB
keyValueStoreIdkey-value store (picker)—Your store that holds the CSV as a record; the picker gives this run read access to it only
recordKeystring—The record's key, e.g. products.csv
modewatch or export—Empty: watch when stateName is given, otherwise export. An explicit mode wins
stateNamestring—Watch list name. Watch mode without a name uses the list default
currencyISO 4217 code—Your store's currency, checked and shown beside every price
decimalCommabooleanfalsePrices use a decimal comma (29,99): read and fixed as 29.99
targetMarginPercentinteger 0–99—Warn when (price − cost per item) / price is under this percent
priceJumpPercentinteger 1–10,00050Watch: a price that moved this much since the last run is a price_jump warning
issueRowsbooleantrueAlso write one free row per issue
writeCleanedCsvbooleantrueSave CLEANED_CSV with the safe fixes
maxItemsinteger 1–200,000100000Most charged variant rows per run; in watch mode the rest come in the next run

Both csvText and a store record may be given: each file is checked on its own (the second cleaned file is CLEANED_CSV_2). Changing decimalComma or targetMarginPercent starts a new watch baseline.

Output

A variant row:

{
"status": "ok",
"attempts": 0,
"error": null,
"input": "csvText",
"rowType": "variant",
"mode": "export",
"watchList": null,
"noDataReason": null,
"sourceRow": 3,
"handle": "pet-gps-tracker",
"sku": "GPS-RED",
"options": "Color: Red",
"price": 29.99,
"compareAtPrice": 24.99,
"costPerItem": null,
"currency": "USD",
"barcodes": ["8712345678902"],
"verdict": "errors",
"qualityScore": 65,
"errorCount": 1,
"warningCount": 2,
"issueCodes": ["gtin_check_digit", "price_currency_symbol", "compare_at_not_higher"],
"issues": [
{ "severity": "warning", "code": "gtin_check_digit", "column": "Barcodes", "sourceRow": 3, "message": "This looks like a GTIN/EAN/UPC but its check digit is wrong: Shopify keeps it as a custom barcode, and Google or Meta will reject it. Confirm it with the manufacturer.", "suggestedValue": null, "fixApplied": false },
{ "severity": "error", "code": "price_currency_symbol", "column": "Price", "sourceRow": 3, "message": "Price holds a currency symbol or code; Shopify wants the number only (9.99).", "suggestedValue": "29.99", "fixApplied": true },
{ "severity": "warning", "code": "compare_at_not_higher", "column": "Compare-at price", "sourceRow": 3, "message": "The compare-at price (24.99) is not above the price (29.99), so no discount shows.", "suggestedValue": null, "fixApplied": false }
],
"changeType": null,
"previousIssueCodes": null,
"priceChangePercent": null,
"url": "https://console.apify.com/storage/key-value-stores/aBcDeFgHiJkLmNoPq",
"source": "Your own product CSV, checked against Shopify's published product CSV format. Not affiliated with or endorsed by Shopify.",
"scrapedAt": "2026-10-05T09:00:00.000Z"
}

(The issue and summary columns of a variant row are null; the dataset's Issues and Summary views show those rows.)

StatusMeaningCharged?
okA checked variant (rowType: "variant"), an issue row or a summary rowVariant rows: export yes; watch baseline, new and changed yes. Issue and summary rows: no
no_dataOnly a header line, image lines only for a handle, no such record, or nothing new since the last runNo
failedThe file can't be read (not a Shopify CSV, not comma-separated, an unclosed quote), a line has the wrong number of values, or the record can't be readNo

The key-value store holds CLEANED_CSV, OUTPUT (each file's summary) and RUN_REPORT (counts, charged and free rows, stop reason).

Issue codes

Errors are what Shopify's import refuses or misreads; warnings import but are likely wrong. ✓ = fixed in CLEANED_CSV.

CodeSeverityWhat it means
unknown_columnerrorA column Shopify's product CSV doesn't have (only Collection may be added)
column_name_casewarning ✓A known column written with other letter case or spacing
duplicate_column, unnamed_columnerrorTwo columns for the same field; a column with no name
barcode_columns_conflicterrorBoth Barcode and Barcodes (Shopify refuses the file)
variant_metafield_columnerrorVariant metafields aren't supported in the product CSV
missing_title_columnerrorNo Title column
mixed_column_formats, line_endings ✓, not_utf8warningOld and new column names mixed; CRLF line ends; text that isn't UTF-8
title_missing, title_too_longerrorNo Title on the product's first line; over 255 characters
handle_invaliderrorA URL handle with spaces or other characters (suggestion only)
handle_missing, handle_not_contiguouswarningNo handle (Shopify makes one); a product's lines not together
duplicate_handleerrorTwo products share a URL handle
title_on_variant_linewarningA later line has another Title (Shopify reads the first)
status_missingerrorThe Status column is present but empty
too_many_tags, too_many_images, too_many_barcodeserrorOver 250 tags, 250 images, 20 barcodes
seo_title_too_long, seo_description_too_longwarningOver 70 / 320 characters
alt_text_too_long, collection_too_longerrorOver 512 / 255 characters
image_url_invaliderrorNot an https:// or http:// image address (checked as text)
image_url_not_https, image_file_name_suffix, duplicate_imagewarninghttp://; a _thumb/_small/_medium file name; the same image twice
option_value_missing, option_name_missing, option_name_changederrorOption names and values that don't match
duplicate_option_combinationerrorTwo variants with the same options
variants_without_options, default_title_with_variantserrorSeveral variants but no option; Default Title with several variants
duplicate_sku, whitespace ✓warningA SKU used twice; spaces around a handle, SKU or barcode
sku_missing_custom_fulfillmenterrorA custom fulfillment service without a SKU
price_currency_symbol ✓, price_thousands_separator ✓error$29.99, 1,299.00
price_decimal_commaerror29,99 (✓ with decimalComma)
price_invalid, price_negativeerrorNot a number (free, 29 dollars); below 0
price_missing, price_zero, price_decimalswarningNo price (Shopify sets 0.00); 0; more than 2 decimals
compare_at_not_higher, cost_above_price, margin_below_targetwarningNo discount shows; a loss on each sale; under your margin target
invalid_booleanerrorNot true/false (✓ for yes/no/1/0)
invalid_enumerrorStatus, inventory policy, tracker, units, fulfillment service (✓ for letter case)
invalid_integer, invalid_number, weight_not_whole_gramserrorNot a whole number (✓ for 10.0); not a number; grams with a unit or decimals (suggested in grams)
packed_size_partialerrorOnly some of the four packed product columns
related_value_missingwarningInventory tracked but the out-of-stock policy blank
gtin_check_digit, gtin_length, isbn_check_digiterror / warningA wrong GS1 check digit or length: an error for a typed barcode (gtin:, upc:, ean:), a warning for an untyped one (Shopify keeps it as a custom barcode, Google and Meta reject it)
barcode_scientific_notationerrorA spreadsheet turned the barcode into 8.71E+12
gtin_leading_zero_lost, duplicate_barcodewarningAn 11-digit UPC (leading zero dropped); one barcode on two variants
price_jumpwarningWatch: the price moved by priceJumpPercent or more since the last run

Output fields

FieldTypeDescription
statusstring (or null)ok = a checked variant (charged in export mode, and in watch mode when it is a baseline, new or changed variant), an issue row or a summary row (both free); no_data = nothing to check (only a header line, image lines only for a handle, no such record, or nothing new since the last run) (free); failed = the file or a line could not be read (free)
attemptsinteger (or null)Reads made for this row: 0 for text in the input, 1 for a key-value store record
errorstring (or null)Why a row is no_data or failed; null on ok rows
inputstring (or null)Which file the row is about: csvText, or the key-value store ID and record key
rowTypestring (or null)variant = one variant line (its verdict and issues); issue = one issue (free); summary = the file (free)
modestring (or null)watch or export
watchListstring (or null)Watch list name (watch mode); null in export mode
noDataReasonstring (or null)Why a row is no_data: empty_file (only a header line), images_only (image lines only for a handle), nothing_new (watch: no change since the last run), not_found (no such record); null otherwise
sourceRowinteger (or null)The line in your file (1 = the header line), as your spreadsheet numbers it
handlestring (or null)The product's URL handle
skustring (or null)The variant's SKU
optionsstring (or null)The variant's options, e.g. Color: Red / Size: S
pricenumber (or null)The price as Shopify would read it (null when it is missing or unreadable)
compareAtPricenumber (or null)The compare-at price as Shopify would read it
costPerItemnumber (or null)The cost per item as Shopify would read it
currencystring (or null)Your store currency (the currency input, ISO 4217), shown beside the prices; null when not given
barcodesarray (or null)The variant's barcodes (from Barcodes or the older Barcode column)
verdictstring (or null)ready (no issue), warnings (warnings only) or errors (at least one error: Shopify would refuse or misread something)
qualityScoreinteger (or null)100 minus 25 per error and 5 per warning (0 to 100)
errorCountinteger (or null)The variant's errors (its product's issues count on its first variant)
warningCountinteger (or null)The variant's warnings
issueCodesarray (or null)The codes of the variant's issues (see the README's issue table)
issuesarray (or null)Each issue: severity, code, column, line, message, suggested value and whether CLEANED_CSV applies it
changeTypestring (or null)Watch mode: baseline (first run of the list), new (a variant the list did not have) or changed (its issues changed, or its price jumped); null in export mode
previousIssueCodesarray (or null)Watch mode: the variant's issue codes at the last run
priceChangePercentnumber (or null)Watch mode: how much the price moved since the last run
severitystring (or null)Issue rows: error (Shopify would refuse or misread it) or warning (it imports, but something is likely wrong)
issueCodestring (or null)Issue rows: the issue code (see the README)
columnstring (or null)Issue rows: the column as named in your header line; null for a whole-line or whole-file issue
valuestring (or null)Issue rows: the cell's value, only for identifiers, numbers and codes (never titles, descriptions or vendors)
messagestring (or null)Issue rows: what is wrong, in one sentence
suggestedValuestring (or null)Issue rows: the value to use instead, when one is clear
fixAppliedboolean (or null)Issue rows: true when CLEANED_CSV already has the suggested value (a safe formatting fix)
relatedRowsarray (or null)Issue rows: the other line(s) involved (the first duplicate, the product line)
rowsReadinteger (or null)Summary: data lines read (the header not counted)
productCountinteger (or null)Summary: products (URL handles)
variantCountinteger (or null)Summary: variant lines checked
imageRowCountinteger (or null)Summary: lines that only add images
variantsReadyinteger (or null)Summary: variants with no issue
variantsWithWarningsinteger (or null)Summary: variants with warnings only
variantsWithErrorsinteger (or null)Summary: variants with at least one error
errorTotalinteger (or null)Summary: all errors (variants, products and file)
warningTotalinteger (or null)Summary: all warnings
fileIssueCountinteger (or null)Summary: issues of the header or the file itself
importReadyboolean (or null)Summary: true when the file has no error and every line could be read
feedQualityScoreinteger (or null)Summary: the variants' average quality score, minus 10 per file error and 5 per unreadable line (0 to 100)
csvFormatstring (or null)Summary: current (today's column names), legacy (the older names Shopify still accepts) or mixed
unknownColumnsarray (or null)Summary: columns that are not in Shopify's product CSV
issueCountsarray (or null)Summary: how many of each issue, most frequent first
cleanedCsvKeystring (or null)Summary: the record in the run's key-value store that holds the cleaned CSV (null when writeCleanedCsv is off)
unchangedCountinteger (or null)Watch mode summary: variants, lines and file issues unchanged since the last run (not returned)
removedCountinteger (or null)Watch mode summary: variants in the last run that are not in this file
urlstring (or null)The run's key-value store in Apify Console, which holds CLEANED_CSV and OUTPUT
sourcestring (or null)Where the data comes from
scrapedAtstring (or null)When the row was written (ISO 8601, UTC)

Pricing

Pay per event: one variant-check event per checked variant (export: every variant; watch: baseline, new and changed variants).

PlanPrice per variantPer 1,000 variants
Free (no discount)$0.0015$1.50
Bronze$0.0013$1.30
Silver$0.00115$1.15
Gold (and Platinum, Diamond)$0.0010$1.00
  • Never charged: failed rows (a file or line that can't be read), no_data rows, issue rows, summary rows, image lines and unchanged variants. You are never charged for failed results.
  • Apify also charges its small per-run start event. There are no usage fees on top, so the actor is eligible for x402 agent payments.

Use it from AI agents

One clear main input, csvText (the CSV as text); every variant row has status, verdict, issueCodes and issues, and the summary row has importReady. Call it through the Apify API, the Apify MCP server (mouadapi/shopify-product-feed-validator) or x402 agentic payments. Copy-paste call (your Apify token in place of YOUR_APIFY_TOKEN):

$curl -X POST "https://api.apify.com/v2/acts/mouadapi~shopify-product-feed-validator/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" -H "Content-Type: application/json" -d '{"csvText": "URL handle,Title,Option1 name,Option1 value,SKU,Barcodes,Price\u000amug,Stoneware Mug,Title,Default Title,MUG-350,4006381333931,12.50\u000a", "currency": "USD"}'
import { ApifyClient } from 'apify-client';
import { readFileSync } from 'node:fs';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('mouadapi/shopify-product-feed-validator').call({ csvText: readFileSync('products.csv', 'utf8'), currency: 'USD' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const notReady = items.filter((r) => r.rowType === 'variant' && r.verdict === 'errors');

Limits

  • Shopify's product CSV only (V1). Google Merchant Center and Meta catalog profiles are planned.
  • Up to 200,000 lines and about 9 MB of text in csvText; a larger file goes in a key-value store record.
  • Image URLs are checked as text: an address that is well formed but doesn't exist is not found (the actor fetches no URL).
  • Barcodes are checked by their GS1 check digit and length only (no barcode database): a valid number can still belong to another product.
  • Watch mode keeps one entry per variant in your own named key-value store; variants removed from the feed are counted in removedCount.

Known issues

  • Shopify's importer can change its messages and its column names (it keeps older names working). The checks follow its published format; an import can still fail for reasons the file doesn't show (an image Shopify can't download, a custom fulfillment service that isn't set up in your admin, a product category Shopify doesn't know).
  • Option values, SKUs, barcodes, prices and handles come back in the rows; titles, descriptions, vendors, tags and other free text never do (they stay in your file and in CLEANED_CSV).

FAQ

Does it change my file? No. Your input is never changed; CLEANED_CSV is a separate copy with formatting fixes only, and every fix is listed on its issue (fixApplied: true).

Why is a price with a comma an error and not fixed? 29,99 and 1,299 look alike; set decimalComma: true if your prices use a decimal comma, and the cleaned CSV fixes them.

Why did a watch run return one no_data row? Nothing changed since the last run of the watch list; the row says how many were unchanged. It is free.

Data and licence