# Shopify Product Search | Cross-Store SKU Match & CSV (`practical_ai_lab_jp/pal-shopify-public-product-search`) Actor

Search public Shopify product feeds for stores you select, using SKU, vendor, tag and price filters. Export CSV and identify shared SKUs across stores. No global store index, currency conversion or private stock quantity.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-shopify-public-product-search.md
- **Developed by:** [Practical AI Lab](https://apify.com/practical_ai_lab_jp) (community)
- **Categories:** E-commerce, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.70 / 1,000 matched product records

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

## Shopify Product Search | Cross-Store SKU Match & CSV

Search the public product feeds of Shopify stores you are permitted to read, filter by SKU, title, vendor, product type, tag and price, and export reproducible JSON (dataset) and CSV files.

### SKU sourcing search report — store-by-store coverage

Start from the **free-to-configure public demo**:
[Shopify Product Search — SKU & Vendor Example](https://apify.com/practical_ai_lab_jp/pal-shopify-public-product-search/examples/search-shopify-products-by-sku-vendor-demo).
Use the fictional inline products to see the output without asking for access to a shop.

For B2B catalog reconciliation, supply one or more *permitted* public Shopify store URLs, then use `skuExact` to audit a list of SKU values. The new `SEARCH_COVERAGE_REPORT` JSON file tells your buying team:

- How many source products were scanned, matching product/variant rows exported and distinct observed SKUs by store.
- Which exact SKUs were **not observed in the pages you were permitted to inspect** (this does not prove the SKU is not sold).
- Which store feed failed, reached a limit, or returned no matching records, with an actionable next step.
- Whether result scope may be limited by `maxPagesPerStore` or `maxResults`.

This is calculated **from existing export rows**, so it does not require more storefront requests or charge an extra event. The output is useful for supplier catalog QA, store-to-store SKU reconnaissance, repeated assortment checks and procurement spreadsheet exports. Cross-store matches are candidates only; verify product identity and store currency separately.

**Example filters:** `{"storeUrls":["https://store-you-can-access.example"],"skuExact":["SKU-100","SKU-200"],"maxPagesPerStore":2,"maxResults":50}`. Replace the example domain and confirm you are authorized to read the store.

### What makes it different

- **SKU-first**: one record per matching variant, with the SKU and a `matchedOn` list that explains why it matched. A product-level mode is also available.
- **Reproducible**: every run writes `RUN_SUMMARY` with an input fingerprint and a results hash; every record carries a `recordHash`, so you can diff two exports and see what changed.
- **Spreadsheet-safe CSV**: `EXPORT_CSV` neutralises cells that start with `=`, `+`, `-` or `@`.
- **Offline mode**: paste product objects you already hold (`inlineProducts`) and search them with the same filters, with no network request.
- **Conservative by design**: https only, public hostnames only, `robots.txt` is checked first, requests are paced and size-limited, redirects are not followed.

### Quick start (copy and run)

The default input searches built-in sample products, so it works without any store:

```json
{
  "query": "outdoor",
  "vendors": ["Harbor & Pine"],
  "granularity": "variant",
  "maxResults": 100
}
```

To read a live store you are permitted to collect from:

```json
{
  "storeUrls": ["https://shop.example.com"],
  "skuContains": ["TRS-"],
  "onlyAvailable": true,
  "maxPagesPerStore": 4,
  "requestDelayMs": 1000,
  "maxResults": 200
}
```

Replace `shop.example.com` with a real store. You are responsible for confirming you may use that store's data.

### Input

| Field | Meaning |
| --- | --- |
| `storeUrls` | Up to 10 store root URLs or domains. |
| `inlineProducts` | Product objects in Shopify `products.json` format (up to 5000). |
| `query` | All words must appear in SKU, title, variant title, vendor, type, tags or handle. |
| `skuContains` / `skuExact` | SKU fragments or exact SKUs (case-insensitive). |
| `vendors`, `productTypes`, `tags` | Exact vendor/type match; any-of tags. |
| `minPrice`, `maxPrice` | Variant price bounds. |
| `onlyAvailable` | Keep variants marked available. |
| `granularity` | `variant` (default) or `product`. |
| `maxResults` | 1-5000 records. |
| `maxPagesPerStore` | 1-20 pages of up to 250 products. |
| `requestDelayMs` | 500-10000 ms between requests. |
| `writeCsv` | Save `EXPORT_CSV`. |

### Output

Dataset records contain `store`, `productId`, `variantId`, `sku`, `title`, `variantTitle`, `vendor`, `productType`, `tags`, `price`, `compareAtPrice`, `available`, `matchedOn`, `matchedVariantCount`, `productUrl`, `imageUrl`, `updatedAt` and `recordHash`.

Key-value store: `EXPORT_CSV` (same records) and `RUN_SUMMARY` (fingerprint, results hash, per-store status, caveats).

Example record:

```json
{
  "store": "inline-data",
  "sku": "HP-BTL-750-BL",
  "title": "Insulated Water Bottle",
  "vendor": "Harbor & Pine",
  "price": "24.50",
  "available": true,
  "matchedOn": ["tags"],
  "matchedVariantCount": 1
}
```

### Limitations and sample-size caveats

- Results are a **snapshot** of a public feed at run time, not a full or guaranteed-complete catalog. Only the first `maxPagesPerStore` pages are read, so large catalogs can be partly scanned; check `productsScanned` and `stopReason` in `RUN_SUMMARY`.
- Many stores disable `/products.json`, disallow it in `robots.txt`, rate-limit, or redirect. Those stores are skipped with a reason, never bypassed. If `robots.txt` cannot be read cleanly, the store is skipped.
- Feeds do not include currency, tax, shipping or stock counts. `available` is a flag, not a quantity.
- Feed ordering can change between runs, so identical filters may return a different first N records on a changing store. Compare `recordHash` values rather than row positions.
- Hostname safety is checked by DNS before each store is read; this reduces but cannot fully eliminate DNS-rebinding risk.
- Searches are plain text matching, with no fuzzy matching or synonyms.
- This Actor has not been validated as having paying demand; treat any adoption figures for similar tools as unproven.

### Responsible use

Only collect data from stores where you have the right to do so, and follow each store's terms. The Actor does not collect personal data, log in, use proxies or read private storefront areas.

### Billing

The code emits one `product-record` event per exported record, after the record is stored. Runs that fail validation or stores that error produce no events. The Apify Store Pricing tab shows the current pay-per-event plan.

### Local tests

`python -m unittest discover -s tests` runs offline tests of the filtering, URL safety, robots handling and CSV logic.

### Cross-store SKU overlap report (new)

When multiple permitted Shopify store feeds are scanned, the Actor now writes
`SKU_CROSS_STORE_AUDIT` in Key-value storage and a `crossStoreSkuAudit`
section in `RUN_SUMMARY`. These identify normalized SKUs that appear in
**two or more different stores**, with each published product title, product
URL, price display and availability flag.

**B2B use case:** Identify candidate equivalent products across supplier or
retailer catalogs, export the overlap report for merchant review, and rerun
after each catalog refresh. Unlike raw search results, the report groups the
same SKU across stores and preserves the source links.

**Important:** A shared SKU alone does not prove identical products. Published
Shopify feeds do not reveal currency, tax, shipping or private stock quantity.
The tool does **not** calculate cross-store price winners or claim normalized
prices. Inline sample-only mode is excluded from cross-store comparisons.
This feature creates no additional paid event: billing is still per exported
product record as shown in the Store Pricing tab.

# Actor input Schema

## `storeUrls` (type: `array`):

Public https store root URLs or domains (up to 10). Only /products.json is read, and only if robots.txt allows it. Leave empty to search the inline sample data instead. Only use stores whose data you are permitted to collect.

## `inlineProducts` (type: `array`):

Optional list of product objects in Shopify products.json format that you already hold. They are searched with the same filters and no network request is made for them.

## `query` (type: `string`):

Words that must all appear in the SKU, title, variant title, vendor, product type, tags or handle (case-insensitive).

## `skuContains` (type: `array`):

Keep variants whose SKU contains any of these fragments.

## `skuExact` (type: `array`):

Keep variants whose SKU equals any of these values (case-insensitive).

## `vendors` (type: `array`):

Keep only these vendors (exact, case-insensitive).

## `productTypes` (type: `array`):

Keep only these product types (exact, case-insensitive).

## `tags` (type: `array`):

Keep products carrying at least one of these tags.

## `minPrice` (type: `number`):

Variant price lower bound, in the store's own currency.

## `maxPrice` (type: `number`):

Variant price upper bound, in the store's own currency.

## `onlyAvailable` (type: `boolean`):

Keep only variants the feed marks as available.

## `granularity` (type: `string`):

One record per matching variant (best for SKU lists) or one record per matching product.

## `maxResults` (type: `integer`):

Hard cap on exported records (1-5000). Also the cap on billable records.

## `maxPagesPerStore` (type: `integer`):

Feed pages of up to 250 products read per store (1-20).

## `requestDelayMs` (type: `integer`):

Pause before each feed request. Minimum 500 ms to stay polite.

## `writeCsv` (type: `boolean`):

Also save EXPORT_CSV to the key-value store.

## Actor input object example

```json
{
  "storeUrls": [],
  "inlineProducts": [
    {
      "id": 1001,
      "title": "Trail Running Shoe",
      "handle": "trail-running-shoe",
      "vendor": "Northfield",
      "product_type": "Footwear",
      "tags": [
        "running",
        "trail"
      ],
      "updated_at": "2026-01-15T09:00:00Z",
      "variants": [
        {
          "id": 5001,
          "title": "Size 9",
          "sku": "NF-TRS-09",
          "price": "89.00",
          "compare_at_price": "110.00",
          "available": true
        },
        {
          "id": 5002,
          "title": "Size 10",
          "sku": "NF-TRS-10",
          "price": "89.00",
          "compare_at_price": null,
          "available": false
        }
      ]
    },
    {
      "id": 1002,
      "title": "Insulated Water Bottle",
      "handle": "insulated-water-bottle",
      "vendor": "Harbor & Pine",
      "product_type": "Drinkware",
      "tags": "steel, outdoor",
      "updated_at": "2026-02-02T12:30:00Z",
      "variants": [
        {
          "id": 5003,
          "title": "Blue 750ml",
          "sku": "HP-BTL-750-BL",
          "price": "24.50",
          "compare_at_price": null,
          "available": true
        }
      ]
    },
    {
      "id": 1003,
      "title": "Canvas Daypack",
      "handle": "canvas-daypack",
      "vendor": "Harbor & Pine",
      "product_type": "Bags",
      "tags": [
        "outdoor",
        "bag"
      ],
      "updated_at": "2026-02-10T08:15:00Z",
      "variants": [
        {
          "id": 5004,
          "title": "Default",
          "sku": "HP-PACK-01",
          "price": "59.00",
          "compare_at_price": null,
          "available": true
        }
      ]
    }
  ],
  "query": "outdoor",
  "vendors": [
    "Harbor & Pine"
  ],
  "onlyAvailable": false,
  "granularity": "variant",
  "maxResults": 100,
  "maxPagesPerStore": 4,
  "requestDelayMs": 1000,
  "writeCsv": true
}
```

# Actor output Schema

## `results` (type: `string`):

Structured records from this Actor run; downloadable as JSON or CSV.

# 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 = {
    "storeUrls": [],
    "inlineProducts": [
        {
            "id": 1001,
            "title": "Trail Running Shoe",
            "handle": "trail-running-shoe",
            "vendor": "Northfield",
            "product_type": "Footwear",
            "tags": [
                "running",
                "trail"
            ],
            "updated_at": "2026-01-15T09:00:00Z",
            "variants": [
                {
                    "id": 5001,
                    "title": "Size 9",
                    "sku": "NF-TRS-09",
                    "price": "89.00",
                    "compare_at_price": "110.00",
                    "available": true
                },
                {
                    "id": 5002,
                    "title": "Size 10",
                    "sku": "NF-TRS-10",
                    "price": "89.00",
                    "compare_at_price": null,
                    "available": false
                }
            ]
        },
        {
            "id": 1002,
            "title": "Insulated Water Bottle",
            "handle": "insulated-water-bottle",
            "vendor": "Harbor & Pine",
            "product_type": "Drinkware",
            "tags": "steel, outdoor",
            "updated_at": "2026-02-02T12:30:00Z",
            "variants": [
                {
                    "id": 5003,
                    "title": "Blue 750ml",
                    "sku": "HP-BTL-750-BL",
                    "price": "24.50",
                    "compare_at_price": null,
                    "available": true
                }
            ]
        },
        {
            "id": 1003,
            "title": "Canvas Daypack",
            "handle": "canvas-daypack",
            "vendor": "Harbor & Pine",
            "product_type": "Bags",
            "tags": [
                "outdoor",
                "bag"
            ],
            "updated_at": "2026-02-10T08:15:00Z",
            "variants": [
                {
                    "id": 5004,
                    "title": "Default",
                    "sku": "HP-PACK-01",
                    "price": "59.00",
                    "compare_at_price": null,
                    "available": true
                }
            ]
        }
    ],
    "query": "outdoor",
    "vendors": [
        "Harbor & Pine"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-shopify-public-product-search").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 = {
    "storeUrls": [],
    "inlineProducts": [
        {
            "id": 1001,
            "title": "Trail Running Shoe",
            "handle": "trail-running-shoe",
            "vendor": "Northfield",
            "product_type": "Footwear",
            "tags": [
                "running",
                "trail",
            ],
            "updated_at": "2026-01-15T09:00:00Z",
            "variants": [
                {
                    "id": 5001,
                    "title": "Size 9",
                    "sku": "NF-TRS-09",
                    "price": "89.00",
                    "compare_at_price": "110.00",
                    "available": True,
                },
                {
                    "id": 5002,
                    "title": "Size 10",
                    "sku": "NF-TRS-10",
                    "price": "89.00",
                    "compare_at_price": None,
                    "available": False,
                },
            ],
        },
        {
            "id": 1002,
            "title": "Insulated Water Bottle",
            "handle": "insulated-water-bottle",
            "vendor": "Harbor & Pine",
            "product_type": "Drinkware",
            "tags": "steel, outdoor",
            "updated_at": "2026-02-02T12:30:00Z",
            "variants": [{
                    "id": 5003,
                    "title": "Blue 750ml",
                    "sku": "HP-BTL-750-BL",
                    "price": "24.50",
                    "compare_at_price": None,
                    "available": True,
                }],
        },
        {
            "id": 1003,
            "title": "Canvas Daypack",
            "handle": "canvas-daypack",
            "vendor": "Harbor & Pine",
            "product_type": "Bags",
            "tags": [
                "outdoor",
                "bag",
            ],
            "updated_at": "2026-02-10T08:15:00Z",
            "variants": [{
                    "id": 5004,
                    "title": "Default",
                    "sku": "HP-PACK-01",
                    "price": "59.00",
                    "compare_at_price": None,
                    "available": True,
                }],
        },
    ],
    "query": "outdoor",
    "vendors": ["Harbor & Pine"],
}

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-shopify-public-product-search").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 '{
  "storeUrls": [],
  "inlineProducts": [
    {
      "id": 1001,
      "title": "Trail Running Shoe",
      "handle": "trail-running-shoe",
      "vendor": "Northfield",
      "product_type": "Footwear",
      "tags": [
        "running",
        "trail"
      ],
      "updated_at": "2026-01-15T09:00:00Z",
      "variants": [
        {
          "id": 5001,
          "title": "Size 9",
          "sku": "NF-TRS-09",
          "price": "89.00",
          "compare_at_price": "110.00",
          "available": true
        },
        {
          "id": 5002,
          "title": "Size 10",
          "sku": "NF-TRS-10",
          "price": "89.00",
          "compare_at_price": null,
          "available": false
        }
      ]
    },
    {
      "id": 1002,
      "title": "Insulated Water Bottle",
      "handle": "insulated-water-bottle",
      "vendor": "Harbor & Pine",
      "product_type": "Drinkware",
      "tags": "steel, outdoor",
      "updated_at": "2026-02-02T12:30:00Z",
      "variants": [
        {
          "id": 5003,
          "title": "Blue 750ml",
          "sku": "HP-BTL-750-BL",
          "price": "24.50",
          "compare_at_price": null,
          "available": true
        }
      ]
    },
    {
      "id": 1003,
      "title": "Canvas Daypack",
      "handle": "canvas-daypack",
      "vendor": "Harbor & Pine",
      "product_type": "Bags",
      "tags": [
        "outdoor",
        "bag"
      ],
      "updated_at": "2026-02-10T08:15:00Z",
      "variants": [
        {
          "id": 5004,
          "title": "Default",
          "sku": "HP-PACK-01",
          "price": "59.00",
          "compare_at_price": null,
          "available": true
        }
      ]
    }
  ],
  "query": "outdoor",
  "vendors": [
    "Harbor & Pine"
  ]
}' |
apify call practical_ai_lab_jp/pal-shopify-public-product-search --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,practical_ai_lab_jp/pal-shopify-public-product-search"
        }
    }
}
```

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/Y2ZuEjmHYwX4FGWRg/builds/E8mGM6G0Ap45D8Ub6/openapi.json
