# Shopify Product CSV Validator & Cleaner (`mouadapi/shopify-product-feed-validator`) Actor

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.

- **URL**: https://apify.com/mouadapi/shopify-product-feed-validator.md
- **Developed by:** [COMPASS DEV](https://apify.com/mouadapi) (community)
- **Categories:** E-commerce, Developer tools
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 variant checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

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:

```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:

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

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `csvText` | text | — (prefilled with a small sample) | Your Shopify product CSV, header line first; up to about 9 MB |
| `keyValueStoreId` | key-value store (picker) | — | Your store that holds the CSV as a record; the picker gives this run read access to it only |
| `recordKey` | string | — | The record's key, e.g. `products.csv` |
| `mode` | `watch` or `export` | — | Empty: watch when `stateName` is given, otherwise export. An explicit mode wins |
| `stateName` | string | — | Watch list name. Watch mode without a name uses the list `default` |
| `currency` | ISO 4217 code | — | Your store's currency, checked and shown beside every price |
| `decimalComma` | boolean | `false` | Prices use a decimal comma (29,99): read and fixed as 29.99 |
| `targetMarginPercent` | integer 0–99 | — | Warn when (price − cost per item) / price is under this percent |
| `priceJumpPercent` | integer 1–10,000 | `50` | Watch: a price that moved this much since the last run is a `price_jump` warning |
| `issueRows` | boolean | `true` | Also write one free row per issue |
| `writeCleanedCsv` | boolean | `true` | Save `CLEANED_CSV` with the safe fixes |
| `maxItems` | integer 1–200,000 | `100000` | Most 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:

```json
{
    "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.)

| Status | Meaning | Charged? |
|---|---|---|
| `ok` | A checked variant (`rowType: "variant"`), an issue row or a summary row | Variant rows: export yes; watch `baseline`, `new` and `changed` yes. Issue and summary rows: no |
| `no_data` | Only a header line, image lines only for a handle, no such record, or nothing new since the last run | No |
| `failed` | The 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 read | No |

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`.

| Code | Severity | What it means |
|---|---|---|
| `unknown_column` | error | A column Shopify's product CSV doesn't have (only `Collection` may be added) |
| `column_name_case` | warning ✓ | A known column written with other letter case or spacing |
| `duplicate_column`, `unnamed_column` | error | Two columns for the same field; a column with no name |
| `barcode_columns_conflict` | error | Both `Barcode` and `Barcodes` (Shopify refuses the file) |
| `variant_metafield_column` | error | Variant metafields aren't supported in the product CSV |
| `missing_title_column` | error | No `Title` column |
| `mixed_column_formats`, `line_endings` ✓, `not_utf8` | warning | Old and new column names mixed; CRLF line ends; text that isn't UTF-8 |
| `title_missing`, `title_too_long` | error | No Title on the product's first line; over 255 characters |
| `handle_invalid` | error | A URL handle with spaces or other characters (suggestion only) |
| `handle_missing`, `handle_not_contiguous` | warning | No handle (Shopify makes one); a product's lines not together |
| `duplicate_handle` | error | Two products share a URL handle |
| `title_on_variant_line` | warning | A later line has another Title (Shopify reads the first) |
| `status_missing` | error | The Status column is present but empty |
| `too_many_tags`, `too_many_images`, `too_many_barcodes` | error | Over 250 tags, 250 images, 20 barcodes |
| `seo_title_too_long`, `seo_description_too_long` | warning | Over 70 / 320 characters |
| `alt_text_too_long`, `collection_too_long` | error | Over 512 / 255 characters |
| `image_url_invalid` | error | Not an https:// or http:// image address (checked as text) |
| `image_url_not_https`, `image_file_name_suffix`, `duplicate_image` | warning | http://; a `_thumb`/`_small`/`_medium` file name; the same image twice |
| `option_value_missing`, `option_name_missing`, `option_name_changed` | error | Option names and values that don't match |
| `duplicate_option_combination` | error | Two variants with the same options |
| `variants_without_options`, `default_title_with_variants` | error | Several variants but no option; `Default Title` with several variants |
| `duplicate_sku`, `whitespace` ✓ | warning | A SKU used twice; spaces around a handle, SKU or barcode |
| `sku_missing_custom_fulfillment` | error | A custom fulfillment service without a SKU |
| `price_currency_symbol` ✓, `price_thousands_separator` ✓ | error | `$29.99`, `1,299.00` |
| `price_decimal_comma` | error | `29,99` (✓ with `decimalComma`) |
| `price_invalid`, `price_negative` | error | Not a number (`free`, `29 dollars`); below 0 |
| `price_missing`, `price_zero`, `price_decimals` | warning | No price (Shopify sets 0.00); 0; more than 2 decimals |
| `compare_at_not_higher`, `cost_above_price`, `margin_below_target` | warning | No discount shows; a loss on each sale; under your margin target |
| `invalid_boolean` | error | Not true/false (✓ for yes/no/1/0) |
| `invalid_enum` | error | Status, inventory policy, tracker, units, fulfillment service (✓ for letter case) |
| `invalid_integer`, `invalid_number`, `weight_not_whole_grams` | error | Not a whole number (✓ for `10.0`); not a number; grams with a unit or decimals (suggested in grams) |
| `packed_size_partial` | error | Only some of the four packed product columns |
| `related_value_missing` | warning | Inventory tracked but the out-of-stock policy blank |
| `gtin_check_digit`, `gtin_length`, `isbn_check_digit` | error / warning | A 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_notation` | error | A spreadsheet turned the barcode into `8.71E+12` |
| `gtin_leading_zero_lost`, `duplicate_barcode` | warning | An 11-digit UPC (leading zero dropped); one barcode on two variants |
| `price_jump` | warning | Watch: the price moved by `priceJumpPercent` or more since the last run |

### Output fields

| Field | Type | Description |
|---|---|---|
| `status` | string (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) |
| `attempts` | integer (or null) | Reads made for this row: 0 for text in the input, 1 for a key-value store record |
| `error` | string (or null) | Why a row is no_data or failed; null on ok rows |
| `input` | string (or null) | Which file the row is about: csvText, or the key-value store ID and record key |
| `rowType` | string (or null) | variant = one variant line (its verdict and issues); issue = one issue (free); summary = the file (free) |
| `mode` | string (or null) | watch or export |
| `watchList` | string (or null) | Watch list name (watch mode); null in export mode |
| `noDataReason` | string (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 |
| `sourceRow` | integer (or null) | The line in your file (1 = the header line), as your spreadsheet numbers it |
| `handle` | string (or null) | The product's URL handle |
| `sku` | string (or null) | The variant's SKU |
| `options` | string (or null) | The variant's options, e.g. Color: Red / Size: S |
| `price` | number (or null) | The price as Shopify would read it (null when it is missing or unreadable) |
| `compareAtPrice` | number (or null) | The compare-at price as Shopify would read it |
| `costPerItem` | number (or null) | The cost per item as Shopify would read it |
| `currency` | string (or null) | Your store currency (the currency input, ISO 4217), shown beside the prices; null when not given |
| `barcodes` | array (or null) | The variant's barcodes (from Barcodes or the older Barcode column) |
| `verdict` | string (or null) | ready (no issue), warnings (warnings only) or errors (at least one error: Shopify would refuse or misread something) |
| `qualityScore` | integer (or null) | 100 minus 25 per error and 5 per warning (0 to 100) |
| `errorCount` | integer (or null) | The variant's errors (its product's issues count on its first variant) |
| `warningCount` | integer (or null) | The variant's warnings |
| `issueCodes` | array (or null) | The codes of the variant's issues (see the README's issue table) |
| `issues` | array (or null) | Each issue: severity, code, column, line, message, suggested value and whether CLEANED_CSV applies it |
| `changeType` | string (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 |
| `previousIssueCodes` | array (or null) | Watch mode: the variant's issue codes at the last run |
| `priceChangePercent` | number (or null) | Watch mode: how much the price moved since the last run |
| `severity` | string (or null) | Issue rows: error (Shopify would refuse or misread it) or warning (it imports, but something is likely wrong) |
| `issueCode` | string (or null) | Issue rows: the issue code (see the README) |
| `column` | string (or null) | Issue rows: the column as named in your header line; null for a whole-line or whole-file issue |
| `value` | string (or null) | Issue rows: the cell's value, only for identifiers, numbers and codes (never titles, descriptions or vendors) |
| `message` | string (or null) | Issue rows: what is wrong, in one sentence |
| `suggestedValue` | string (or null) | Issue rows: the value to use instead, when one is clear |
| `fixApplied` | boolean (or null) | Issue rows: true when CLEANED_CSV already has the suggested value (a safe formatting fix) |
| `relatedRows` | array (or null) | Issue rows: the other line(s) involved (the first duplicate, the product line) |
| `rowsRead` | integer (or null) | Summary: data lines read (the header not counted) |
| `productCount` | integer (or null) | Summary: products (URL handles) |
| `variantCount` | integer (or null) | Summary: variant lines checked |
| `imageRowCount` | integer (or null) | Summary: lines that only add images |
| `variantsReady` | integer (or null) | Summary: variants with no issue |
| `variantsWithWarnings` | integer (or null) | Summary: variants with warnings only |
| `variantsWithErrors` | integer (or null) | Summary: variants with at least one error |
| `errorTotal` | integer (or null) | Summary: all errors (variants, products and file) |
| `warningTotal` | integer (or null) | Summary: all warnings |
| `fileIssueCount` | integer (or null) | Summary: issues of the header or the file itself |
| `importReady` | boolean (or null) | Summary: true when the file has no error and every line could be read |
| `feedQualityScore` | integer (or null) | Summary: the variants' average quality score, minus 10 per file error and 5 per unreadable line (0 to 100) |
| `csvFormat` | string (or null) | Summary: current (today's column names), legacy (the older names Shopify still accepts) or mixed |
| `unknownColumns` | array (or null) | Summary: columns that are not in Shopify's product CSV |
| `issueCounts` | array (or null) | Summary: how many of each issue, most frequent first |
| `cleanedCsvKey` | string (or null) | Summary: the record in the run's key-value store that holds the cleaned CSV (null when writeCleanedCsv is off) |
| `unchangedCount` | integer (or null) | Watch mode summary: variants, lines and file issues unchanged since the last run (not returned) |
| `removedCount` | integer (or null) | Watch mode summary: variants in the last run that are not in this file |
| `url` | string (or null) | The run's key-value store in Apify Console, which holds CLEANED_CSV and OUTPUT |
| `source` | string (or null) | Where the data comes from |
| `scrapedAt` | string (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).

| Plan | Price per variant | Per 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`):

```bash
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"}'
```

```js
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

- Input: your own file. It is read only in this run (and, in watch mode, its variants' issue codes and prices are kept in
  your own watch list store).
- The rules follow Shopify's published product CSV format
  (https://help.shopify.com/en/manual/products/import-export/using-csv). This actor is not affiliated with or endorsed by
  Shopify.

# Actor input Schema

## `csvText` (type: `string`):

Your Shopify product CSV, pasted as text, header line first (current or older Shopify column names). Up to about 9 MB; for a larger file, use the key-value store fields below. The prefill is a small sample with problems to show what comes back.

## `keyValueStoreId` (type: `string`):

Your key-value store that holds the CSV file as a record (for large files, or files another Actor wrote). The picker gives this run read access to that store only. Give the record key below.

## `recordKey` (type: `string`):

The key of the record that holds the CSV in that store, e.g. products.csv.

## `mode` (type: `string`):

Empty: watch when a watch list name is given, otherwise export. "export" returns every variant; "watch" returns only variants that are new or whose issues or price changed since the last run of the watch list. An explicit mode wins.

## `stateName` (type: `string`):

Name of your watch list (kept in your own storage between runs): upload each new version of the feed and run with the same name to see only what changed. Giving a name turns on watch mode; watch mode without a name uses the list "default".

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

Your store's currency as an ISO 4217 code (USD, EUR, GBP, MAD…): checked, and shown beside every price (Shopify's CSV prices carry no currency).

## `decimalComma` (type: `boolean`):

On: a price written 29,99 is read as 29.99 and fixed in CLEANED_CSV. Off: it is an error with 29.99 as a suggestion only (a comma could also be a thousands separator).

## `targetMarginPercent` (type: `integer`):

Warn when (price − cost per item) / price is under this percent. Empty: no margin warning (a cost above the price is always a warning).

## `priceJumpPercent` (type: `integer`):

Watch mode: a price that moved by this many percent or more since the last run is a price_jump warning, and the variant comes back as changed.

## `issueRows` (type: `boolean`):

Also write one free row per issue (rowType "issue"), handy in a spreadsheet. The variant rows list their issues either way.

## `writeCleanedCsv` (type: `boolean`):

Save CLEANED_CSV in the run's key-value store: your file with only the safe fixes (trimmed cells; column name case; true/false; lowercase status and other values; prices without currency symbols or thousands separators; whole numbers; UTF-8 with LF line ends). Nothing else is changed.

## `maxItems` (type: `integer`):

Most variant rows returned and charged per run. In watch mode the rest come in the next run.

## Actor input object example

```json
{
  "csvText": "URL handle,Title,Option1 name,Option1 value,SKU,Barcodes,Price,Compare-at price,Status,Product image URL\npet-gps-tracker,Smart GPS Pet Tracker,Color,Black,GPS-BLACK,8712345678906,29.99,39.99,active,https://cdn.example.com/files/gps-black.jpg\npet-gps-tracker,,,Red,GPS-RED,8712345678902,$29.99,24.99,,https://cdn.example.com/files/gps-red.jpg\npet-gps-tracker,,,Red,GPS-RED,,29.99,,,\nstoneware-mug,Stoneware Mug,Title,Default Title,MUG-350,4006381333931,12.50,,Active,http://cdn.example.com/files/mug.jpg\n",
  "decimalComma": false,
  "priceJumpPercent": 50,
  "issueRows": true,
  "writeCleanedCsv": true,
  "maxItems": 100000
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset: variant rows (verdict, issue codes, prices), issue rows and a summary row (rowType tells them apart)

## `cleanedCsv` (type: `string`):

Your file with only the safe fixes applied (writeCleanedCsv on)

## `summary` (type: `string`):

Each file's summary: counts, the quality score, the issue counts

## `runReport` (type: `string`):

Summary of the run (counts, charged and free rows, stop reason)

# 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 = {
    "csvText": `URL handle,Title,Option1 name,Option1 value,SKU,Barcodes,Price,Compare-at price,Status,Product image URL
pet-gps-tracker,Smart GPS Pet Tracker,Color,Black,GPS-BLACK,8712345678906,29.99,39.99,active,https://cdn.example.com/files/gps-black.jpg
pet-gps-tracker,,,Red,GPS-RED,8712345678902,$29.99,24.99,,https://cdn.example.com/files/gps-red.jpg
pet-gps-tracker,,,Red,GPS-RED,,29.99,,,
stoneware-mug,Stoneware Mug,Title,Default Title,MUG-350,4006381333931,12.50,,Active,http://cdn.example.com/files/mug.jpg`
};

// Run the Actor and wait for it to finish
const run = await client.actor("mouadapi/shopify-product-feed-validator").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 = { "csvText": """URL handle,Title,Option1 name,Option1 value,SKU,Barcodes,Price,Compare-at price,Status,Product image URL
pet-gps-tracker,Smart GPS Pet Tracker,Color,Black,GPS-BLACK,8712345678906,29.99,39.99,active,https://cdn.example.com/files/gps-black.jpg
pet-gps-tracker,,,Red,GPS-RED,8712345678902,$29.99,24.99,,https://cdn.example.com/files/gps-red.jpg
pet-gps-tracker,,,Red,GPS-RED,,29.99,,,
stoneware-mug,Stoneware Mug,Title,Default Title,MUG-350,4006381333931,12.50,,Active,http://cdn.example.com/files/mug.jpg
""" }

# Run the Actor and wait for it to finish
run = client.actor("mouadapi/shopify-product-feed-validator").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 '{
  "csvText": "URL handle,Title,Option1 name,Option1 value,SKU,Barcodes,Price,Compare-at price,Status,Product image URL\\npet-gps-tracker,Smart GPS Pet Tracker,Color,Black,GPS-BLACK,8712345678906,29.99,39.99,active,https://cdn.example.com/files/gps-black.jpg\\npet-gps-tracker,,,Red,GPS-RED,8712345678902,$29.99,24.99,,https://cdn.example.com/files/gps-red.jpg\\npet-gps-tracker,,,Red,GPS-RED,,29.99,,,\\nstoneware-mug,Stoneware Mug,Title,Default Title,MUG-350,4006381333931,12.50,,Active,http://cdn.example.com/files/mug.jpg\\n"
}' |
apify call mouadapi/shopify-product-feed-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mouadapi/shopify-product-feed-validator"
        }
    }
}
```

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/3fjNaBCXkw7WdBrFH/builds/QMnbfaWxD4qxZx9Fu/openapi.json
