# Shopify Store Scraper — Product Catalogue & Store API (`plainfetch/shopify-store-scraper`) Actor

Scrape any Shopify store's full product catalogue (title, price, variants, stock, images, tags) plus a store summary (vendors, product types, price range) straight from the store's own public JSON endpoints. You only pay per product and per store summary delivered.

- **URL**: https://apify.com/plainfetch/shopify-store-scraper.md
- **Developed by:** [Plainfetch Studio](https://apify.com/plainfetch) (community)
- **Categories:** E-commerce, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 products

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

Point this at any [Shopify](https://www.shopify.com) store and get back its full product catalogue
— title, price, variants, stock, images, tags — plus a store-level summary, without a browser and
without touching a single private endpoint. **Shopify Store Scraper** is a **Shopify Product API
alternative** built directly on the public JSON files every Shopify storefront exposes by design:
give it a store URL and get one dataset item per product, plus one summary item per store, ready to
filter, sort, or feed into another tool.

### What can you do with it?

- **Monitor a competitor's catalogue.** Track a competitor's product list, prices, and stock status
  over time — run it on a schedule and diff the results.
- **Track prices across many stores.** Pull `priceMin`/`priceMax` for every product across a list of
  stores to build a price-comparison dataset or spot a sale before it's announced.
- **Market research.** Get a store's vendor list, product-type mix, and price range in one
  `store-summary` item — no need to page through the storefront by hand.
- **Feed AI agents and downstream tools.** Call this Actor from the Apify API, an integration, or an
  MCP-connected AI agent so it can look up what a store sells as one step in a larger workflow.

### What data do you get?

Each store produces two kinds of dataset item, distinguished by `type`.

**One `product` item per product:**

| Field                   | Type             | Description                                                                                       |
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------- |
| `title`                 | string           | The product's title.                                                                              |
| `store`                 | string           | The store's origin, e.g. `"https://www.allbirds.com"`.                                            |
| `vendor`                | string | null   | The brand/manufacturer, as set by the store.                                                      |
| `productType`           | string | null   | The store's own product-type label, e.g. `"Shoes"`.                                               |
| `tags`                  | string\[]         | Normalized to an array either way the store serves them.                                          |
| `priceMin` / `priceMax` | number | null   | Lowest/highest variant price.                                                                     |
| `available`             | boolean          | `true` if at least one variant is in stock.                                                       |
| `variantsCount`         | integer          | How many variants the product has (always present).                                               |
| `variants`              | array (optional) | Full variant list — size/color, price, stock, SKU. Omit with an input flag for a lighter dataset. |
| `images`                | string\[]         | Image URLs, straight from the store.                                                              |
| `url`                   | string           | The product page, `https://host/products/<handle>`.                                               |

**One `store-summary` item per store**, built once its catalogue has been read:

| Field                 | Type             | Description                                                                                                                                     |
| --------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                | string | null   | The store's display name, from its `/meta.json`.                                                                                                |
| `currency`            | string | null   | The store's currency, e.g. `"USD"`.                                                                                                             |
| `productsScraped`     | integer          | How many products were actually scraped for this store.                                                                                         |
| `vendors`             | string\[]         | Every distinct vendor seen among the scraped products.                                                                                          |
| `productTypes`        | string\[]         | Every distinct product type seen.                                                                                                               |
| `priceMin`/`priceMax` | number | null   | The store's overall price range across scraped products.                                                                                        |
| `collections`         | array (optional) | Title, handle and product count of each collection. Present only when `includeCollections` is on — and then always present, even when empty.    |
| `collectionsError`    | object | null   | Ships beside `collections`: `null` when the list was read, otherwise why the list is empty (the read failed, or it was skipped).                |
| `complete`            | boolean          | `true` only if the catalogue was read to its natural end — an empty page, or a page with no new products. See below.                            |
| `incompleteReason`    | string | null   | Why `complete` is `false`: `"max-products-reached"`, `"spending-cap-reached"`, or `"interrupted"`. `null` when complete.                        |
| `error`               | object | null   | Set only when `incompleteReason` is `"interrupted"` — the failure that cut the read short, or that stopped the store entirely. See Limitations. |

**Every requested store gets exactly one `store-summary` row — never silently dropped.** Even a
store skipped entirely, because your spending cap was already reached or because this Actor's own
circuit breaker had already tripped before it was ever attempted, still gets a row:
`productsScraped: 0`, `complete: false`, and `incompleteReason` naming why. Neither case is ever
charged.

### What real output looks like

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel. Here is real,
unedited output from a run with `{"storeUrls": ["https://www.allbirds.com"], "maxProductsPerStore":
20}` — the `variants`, `images` and `tags` arrays below are trimmed for readability (the real item
carries all 13 variants and 5 images):

```json
[
  {
    "type": "product",
    "store": "https://www.allbirds.com",
    "id": 7218356060240,
    "title": "Men's Strider Explore - Natural Black (Dark Grey Sole)",
    "handle": "mens-strider-explore",
    "url": "https://www.allbirds.com/products/mens-strider-explore",
    "vendor": "Allbirds",
    "productType": "Shoes",
    "tags": ["allbirds::edition => classic", "allbirds::gender => mens", "shoprunner"],
    "priceMin": 130,
    "priceMax": 130,
    "compareAtPriceMax": null,
    "currency": "USD",
    "available": false,
    "variantsCount": 13,
    "variants": [
      {
        "id": 41334293889104,
        "title": "8",
        "price": 130,
        "compareAtPrice": null,
        "available": false,
        "sku": "A11768M080"
      }
    ],
    "images": ["https://cdn.shopify.com/s/files/1/1104/4168/files/A11768_25Q4_..._PDP_LEFT.png?v=1759336475"],
    "createdAt": "2025-09-10T12:47:47-07:00",
    "updatedAt": "2026-09-20T02:13:47-07:00",
    "publishedAt": "2026-08-26T10:22:14-07:00"
  },
  {
    "type": "store-summary",
    "store": "https://www.allbirds.com",
    "title": "Allbirds",
    "name": "Allbirds",
    "currency": "USD",
    "country": "US",
    "productsScraped": 20,
    "vendors": ["Allbirds", "re:do"],
    "productTypes": ["Shoes", "Socks", "return,package_protection"],
    "priceMin": 0.8,
    "priceMax": 160,
    "complete": false,
    "incompleteReason": "max-products-reached",
    "error": null
  }
]
```

The summary above has `complete: false` because `maxProductsPerStore` (20) was reached before the
store's real catalogue (294 published products) was fully read — it is still a genuinely delivered,
charged result: `incompleteReason` just tells you it isn't the whole store. A summary is only
`complete: true` once the catalogue was read all the way to an empty page.

### How much does it cost?

You pay **$0.001 per product delivered** and **$0.01 per store summary**. The $0.01 store summary
is charged when two things are both true: **at least one product was delivered** for that store,
and **the catalogue read was not cut short by an error**. So a store with an empty catalogue gets
its summary for free, and a summary cut short by a block, a network error, a page too large to
read, this Actor's own page-safety limit, or the run's protection against sustained blocking is
delivered for visibility but **never charged** — and neither is a store that fails outright (not on
Shopify, blocked, or with `products.json` disabled). A summary truncated by your own
`maxProductsPerStore` is still an honest, delivered result and is charged, just labeled
`complete: false`. Once the spending limit you set for the run has been reached, nothing further is
charged at all — summaries included. Products already delivered before an interruption stay charged
either way. No start fee.

| Scraped                         | Cost                                    |
| ------------------------------- | --------------------------------------- |
| 1 store, 800 products           | 800 × $0.001 + $0.01 = **$0.81**        |
| 10 stores, 200 products each    | 2,000 × $0.001 + 10 × $0.01 = **$2.10** |
| 5,000 products across 25 stores | 5,000 × $0.001 + 25 × $0.01 = **$5.25** |

Real example: the run above (`{"storeUrls": ["https://www.allbirds.com"], "maxProductsPerStore":
20}`) delivered 20 products and 1 store summary, for **$0.03**.

### Input

See the **Input** tab for the full configuration. The minimal input is one store URL:

```json
{
  "storeUrls": ["https://www.allbirds.com"]
}
```

- **`storeUrls`** (required) — 1 to 500 Shopify store URLs. The bare domain also works;
  `https://` is added for you. **Duplicate entries are processed once**: the list is reduced to
  each distinct store first, so `store.com`, `https://store.com/` and
  `https://store.com/collections/all` are one store, read and billed once, not three times.
- **`maxProductsPerStore`** (optional, default `1000`, 1 to 25,000) — how many products to scrape
  per store, newest-first page by newest-first page.
- **`includeVariants`** (optional, default `true`) — include each product's full variant list.
  Turn off for a lighter dataset — `variantsCount` and the price range are kept either way.
- **`includeCollections`** (optional, default `false`) — also read `/collections.json` and list the
  store's collections in its summary. When on, the summary always carries both `collections` and
  `collectionsError`, so an empty list is never ambiguous.
- **`requestDelayMs`** (optional, default `250`) — courtesy pause before each request to the same
  store.

### Limitations — read this first

**This Actor requests a minimum of 1 GB of memory.** A `/products.json` or `/collections.json`
page can run up to 12,000,000 characters before this Actor's own body budget cuts it off, and the
built-in per-run concurrency cap reads up to 5 stores at once — that combination needs more
headroom than a 256 MB floor reliably gives.

This Actor only works for a Shopify store that leaves its public `/products.json` endpoint enabled
— the vast majority do, since it's how Shopify's own storefront themes fetch product data, but a
store can disable it. A **password-protected store**, a store that isn't on Shopify, or one that has
turned this endpoint off produces one `store-summary` item with `error.category: "parse"` and the
message `"Not a Shopify store or products.json is disabled"` — and is never charged. A **headless
storefront** built entirely on the Storefront GraphQL API rather than Shopify's own Liquid theme may
not expose `/products.json` either, for the same reason. That message is only ever used for what it
says: a store whose page was simply too large to read within the request budget reports that
instead, so a healthy big catalogue is never mislabeled as a broken store.

**Inventory quantities are not exposed** by Shopify's public JSON — only `available` (in stock or
not) is. Getting an exact quantity would need the Admin API and a private access token, which this
Actor does not use.

**At most 25,000 products per store** (`maxProductsPerStore`'s ceiling) and **at most 250 products
per page**, matching `/products.json`'s own limits. A catalogue larger than `maxProductsPerStore`
still produces a charged summary — just with `complete: false` and `incompleteReason:
"max-products-reached"`, so you always know it isn't the whole store.

**Pagination is guarded against a store that makes no real progress — three layers.** A product
the same store returns again on a later page is delivered and charged only once: product ids are
tracked per store and an exact repeat is dropped before it is ever pushed. A page made entirely of
products already seen is itself the natural end of that catalogue, exactly like an empty page —
this is what stops a host that ignores `?page` (a CDN that strips the query string, a fixed
`products.json`) from serving page 1 forever. As a last-resort backstop beyond those two, no store
ever requests more than `⌈maxProductsPerStore ÷ 250⌉ + 5` pages; hitting that ceiling ends the
store `complete: false`, `incompleteReason: "interrupted"` with an explicit error, while every
product already delivered stays charged and the summary itself is not charged. None of these
guards applies across two *different* stores: the same product in two stores is two rows.

**A block or a network error partway through a store's catalogue delivers a partial, uncharged
summary**, not a silent gap: `complete: false`, `incompleteReason: "interrupted"`, and `error` set
to what stopped it. Every product already scraped before that point stays delivered and charged.
Sites that block automated requests return `error.category: "blocked"` and are never charged for
the affected summary; three consecutive blocked or rate-limited responses (from any endpoint, on
any store) stop the whole run, with a clear message in the log — this Actor never attempts to get
around a block (no proxy rotation, no browser fingerprint spoofing, no CAPTCHA solving). A store
that never got to start at all, because your spending cap was already reached or the breaker had
already tripped, still leaves its own uncharged `store-summary` row rather than vanishing.

### FAQ

**Is this legal?** We can't give you legal advice, but here is exactly what the Actor does: it reads
`/products.json`, `/collections.json` and `/meta.json` — endpoints Shopify publishes on every store
by design, the same data the storefront itself shows shoppers. It does not log in, does not get
around any block, and does not collect personal data (product data has none). You are responsible
for the store URLs you submit and for how you use the results.

**Where does the data come from?** Directly from each store's own domain — `/products.json`,
`/collections.json` and `/meta.json` — no third-party database, no cache.

**How do I use this from the API or an AI agent (MCP)?** Call it like any other Apify Actor: via the
[Apify API](https://docs.apify.com/api/v2), the JavaScript or Python client, an integration, or an
MCP-connected AI agent that has access to your Apify Actors. Pass the same `storeUrls` input and
read the results back from the default dataset.

**What happens if I hit my spending cap?** The run stops cleanly: it finishes the product or store
already being processed, writes everything it has, and reports `chargeLimitReached: true` in the run
summary. Nothing already produced is lost, and nothing beyond the cap is charged.

### Changelog

**0.1** — Initial release.

# Actor input Schema

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

Shopify store URLs to scrape, e.g. "https://www.allbirds.com". The bare domain also works — https:// is added for you. Up to 500 stores per run.

## `maxProductsPerStore` (type: `integer`):

How many products to scrape from each store's catalogue, newest page first. Shopify's own /products.json serves at most 250 per page.

## `includeVariants` (type: `boolean`):

Include each product's full variant list (size/color, price, stock) in the output. Turn off for a lighter dataset — variant count and price range are still included either way.

## `includeCollections` (type: `boolean`):

Also read the store's /collections.json and list its collections (title, handle, product count) in the store summary.

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

Courtesy pause before each request to the same store.

## Actor input object example

```json
{
  "storeUrls": [
    "https://www.allbirds.com"
  ],
  "maxProductsPerStore": 20,
  "includeVariants": true,
  "includeCollections": false,
  "requestDelayMs": 250
}
```

# Actor output Schema

## `products` (type: `string`):

No description

## `stores` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

# 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": [
        "https://www.allbirds.com"
    ],
    "maxProductsPerStore": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("plainfetch/shopify-store-scraper").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": ["https://www.allbirds.com"],
    "maxProductsPerStore": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("plainfetch/shopify-store-scraper").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": [
    "https://www.allbirds.com"
  ],
  "maxProductsPerStore": 20
}' |
apify call plainfetch/shopify-store-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plainfetch/shopify-store-scraper"
        }
    }
}
```

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/PCIELaZdwem4iqYHI/builds/4bBcxpYabBKWBSzs8/openapi.json
