# Shopify Store Products Scraper (`omraydev/shopify-store-products`) Actor

Export every product from any public Shopify store: prices, variants, SKUs, barcodes, stock status, images and collections. Works with custom domains and explains why when a store can't be read.

- **URL**: https://apify.com/omraydev/shopify-store-products.md
- **Developed by:** [Om Ray](https://apify.com/omraydev) (community)
- **Categories:** Agents, Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$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

## Shopify Store Products Scraper

This actor exports the product catalog of any public Shopify store into a dataset, one row per product. Give it a store URL or a bare domain such as `allbirds.com`. Custom domains work, so you don't need to know the store's `.myshopify.com` address.

It reads the JSON feeds every Shopify storefront serves to the public: `/products.json` for the catalog, `/collections.json` and `/collections/<handle>/products.json` for collections, and `/products/<handle>.js` for stock per variant. It doesn't log in or run a browser.

### Input

```json
{
  "stores": ["https://www.allbirds.com"],
  "maxItems": 30,
  "includeVariantAvailability": true,
  "includeCollections": false
}
```

- `stores`: store URLs or domains. A collection URL such as `https://store.com/collections/sale` limits that store to the collection. The same store entered twice is read once, and a product is never saved or charged twice in one run.
- `maxItems`: stop after this many product rows in total, across all stores. The default is 30, so raise it when you want a whole catalog.
- `collections`: optional collection handles or URLs. When set, only products in those collections are saved, for every store in the list.
- `includeVariantAvailability`: on by default. Fetches each product's `.js` page to get stock status per variant, plus barcodes and inventory counts when the store shows them.
- `includeCollections`: off by default. Walks every collection to fill the `collections` field on each product.
- `maxRunMinutes`: the run stops cleanly after this many minutes and keeps what it saved. The default is 60. It also stops about 45 seconds before the Apify run timeout, so the summary always gets written.

### Output

Each product row has the same fields every run. This is a trimmed row from a real local run against allbirds.com on 1 November 2026 (cut down to the first variant, two images and three tags, with the description shortened):

```json
{
  "status": "ok",
  "message": null,
  "inputUrl": "https://www.allbirds.com",
  "storeDomain": "www.allbirds.com",
  "storeName": "Allbirds",
  "id": 6858599596112,
  "handle": "womens-tree-dasher-relay-blizzard-hazy-cobalt",
  "title": "Women's Tree Dasher Relay - Blizzard/Hazy Cobalt (Clarity Blue Sole)",
  "url": "https://www.allbirds.com/products/womens-tree-dasher-relay-blizzard-hazy-cobalt",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "tags": ["allbirds::carbon-score => 5.47", "allbirds::cfId => color-womens-tree-dasher-relay-blzz-hzy-cblt-clrty-bl", "allbirds::complete => true"],
  "createdAt": "2023-07-15T21:42:13-07:00",
  "updatedAt": "2026-11-01T08:09:07-08:00",
  "publishedAt": "2026-09-25T16:49:33-07:00",
  "descriptionText": "Meet The Tree Dasher Relay...",
  "descriptionHtml": "<p>Meet The Tree Dasher Relay...",
  "images": [
    "https://cdn.shopify.com/s/files/1/1104/4168/files/A10691_24Q1_Tree-Dasher-Relay-Blizzard-Hazy-Cobalt-Clarity-Blue_PDP_LEFT.png?v=1776183508",
    "https://cdn.shopify.com/s/files/1/1104/4168/files/A10691_24Q1_Tree-Dasher-Relay-Blizzard-Hazy-Cobalt-Clarity-Blue_PDP_BACK.png?v=1776183507"
  ],
  "options": [{ "name": "Size", "position": 1, "values": ["5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "10.5", "11"] }],
  "collections": null,
  "currency": "USD",
  "priceMin": 67,
  "priceMax": 67,
  "compareAtPriceMin": 135,
  "compareAtPriceMax": 135,
  "available": false,
  "availabilitySource": "product.js",
  "stockCheck": "ok",
  "variantCount": 13,
  "variants": [
    {
      "id": 40234510123088,
      "title": "5",
      "sku": "A10692W050",
      "barcode": "196942084960",
      "price": 67,
      "compareAtPrice": 135,
      "available": false,
      "inventoryQuantity": 0,
      "inventoryPolicy": "deny",
      "inventoryManagement": "shopify",
      "weightGrams": 638,
      "optionValues": { "Size": "5" },
      "imageUrl": null,
      "requiresShipping": true,
      "taxable": true
    }
  ],
  "scrapedAt": "2026-11-01T16:09:10.248Z"
}
```

#### Fields

| Field | Meaning |
| --- | --- |
| `status` | `ok` on charged product rows. `product-page-missing` marks a free product row (see below). Any other value marks a free status row. |
| `message` | Plain explanation on status rows. Null on product rows. |
| `inputUrl` | The store as you typed it. |
| `storeDomain` | Domain the products came from, after redirects. |
| `storeName` | Store name from `/meta.json`, when the store publishes it. |
| `id`, `handle`, `title`, `url` | Product ID, handle, title and page URL. |
| `vendor`, `productType`, `tags` | As set by the store. |
| `createdAt`, `updatedAt`, `publishedAt` | Dates as the store reports them, with its time zone. |
| `descriptionText` | Description as plain text, with paragraphs and list items on their own lines. |
| `descriptionHtml` | Description HTML exactly as served. |
| `images` | Every product image URL. |
| `options` | Options such as Size or Color, with their values. |
| `collections` | Collection handles that contain the product. Null unless `includeCollections` is on. With a collection filter, only the filtered collections are checked. |
| `currency` | Currency code from `/cart.js`, then `/meta.json`, then the home page. |
| `priceMin`, `priceMax` | Lowest and highest variant price. |
| `compareAtPriceMin`, `compareAtPriceMax` | Range of the "was" prices, among variants that have one. |
| `available` | True when at least one variant can be bought. |
| `availabilitySource` | `product.js` when stock came from the product's `.js` page, `products.json` when it came from the catalog feed. |
| `stockCheck` | How the `.js` stock check went: `ok`, `off`, `product-page-404`, `rate-limited`, `timeout` or `failed`. Anything other than `ok` means stock came from the catalog feed. The run summary counts each value. |
| `variantCount` | Number of variants. |
| `variants` | Per variant: `id`, `title`, `sku`, `barcode`, `price`, `compareAtPrice`, `available`, `inventoryQuantity`, `inventoryPolicy`, `inventoryManagement`, `weightGrams`, `optionValues`, `imageUrl`, `requiresShipping`, `taxable`. |
| `scrapedAt` | When the row was saved, in UTC. |

`inventoryQuantity` is only filled when the store actually exposes it in its `.js` feed. Many stores hide it, and then it stays null rather than showing a guess. The same goes for `barcode`. Prices are numbers in the store currency, such as `67` for $67.00.

### When a store can't be read

You never get a silent empty dataset. If a store can't be read, the run still succeeds and saves one free row with a `status` and a `message` that says why:

- `not-shopify`: the site doesn't serve Shopify's product feed and shows no Shopify code.
- `password-protected`: the storefront asks for a password.
- `products-json-disabled`: the site runs on Shopify but has turned off `/products.json`.
- `store-unavailable`: Shopify answers, but no open store exists at that address.
- `blocked`: the site answered 403 or 503, usually a firewall.
- `rate-limited`: the store kept answering 429 after six tries spread over about a minute.
- `http-error`: the site runs on Shopify but `/products.json` gave some other error after retries.
- `unreachable`: the domain doesn't resolve or refuses connections.
- `invalid-input`: the entry isn't a URL or domain.
- `duplicate-store`: the entry is the same store as an earlier one in the list, so it isn't read or charged again.
- `already-saved`: every product this entry found, such as a collection of a store you already listed, was saved earlier in the run.
- `collection-not-found`: a requested collection handle doesn't exist.
- `no-products`: the store is readable but publishes no products.

A few more free rows explain why a store or run ended early:

- `incomplete`: `/meta.json` reports more products than the feed and sitemap gave, or the feed stopped at 25,000 products or ignored page numbers. The message gives both counts.
- `partial`: some pages failed after retries. The products read before that are kept.
- `max-items-reached`, `charge-limit`, `time-limit`: the run hit `maxItems`, your spending limit or the time limit.
- `skipped`: a later store in the list wasn't read because the run had already stopped.
- `collections-incomplete`: not every collection could be read in time for the `collections` field.

When `www.` fails the actor also tries the domain without it, and the other way round. If the home page names the store's `.myshopify.com` address, it tries that too. Some brands put a custom front end on their domain and keep Shopify elsewhere: `www.gymshark.com` is read through `gymshark.com`, and `ruggable.com` through `ruggable.myshopify.com`.

Some stores list products in `/products.json` whose product pages answer 404. Those rows are saved with status `product-page-missing` and `stockCheck` set to `product-page-404`, and they are not charged, since they are probably not for sale. Their stock comes from the catalog feed.

The run summary, with one entry per store and a count of each `stockCheck` value, is saved as `OUTPUT` in the key-value store.

If Apify migrates or restarts the run, the actor reads the product IDs already in the dataset and carries on from there. Products saved before the restart are not saved or charged again.

### Price

You pay $0.001 per product row with status `ok`, which is $1 per 1,000 products. Status rows and `product-page-missing` rows are free. The run stops when it reaches `maxItems` or the spending limit you set on the run, whichever comes first.

### Speed and limits

Shopify throttles each visitor to roughly one request per second once a short burst is used up. The catalog feed returns 250 products per request, so a run without the stock check is fast: all 10,410 products from gymshark.com took 82 seconds in a local test on 1 November 2026. With `includeVariantAvailability` on, the actor makes one extra request per product, at about one product per second after the first few dozen. All 356 Allbirds products took 6 minutes 20 seconds in the same test. Your times will vary with the store and with how busy its servers are. For catalogs in the tens of thousands, either turn the stock check off or raise `maxRunMinutes`.

Shopify's `/products.json` stops at page 100, which is 25,000 products. For bigger stores, or whenever `/meta.json` reports more products than the feed returned, the actor reads the store's product sitemap and fetches the missing products one by one.

`includeCollections` reads every collection on the store before saving products. Stores with hundreds of collections can spend several minutes on this, and the actor gives it at most half of the time limit.

Country stores such as `ca.rhodeskin.com` usually share one Shopify store with the main domain, so the actor treats them as the same store and its prices come from whichever address it read.

Only products published to the online store are visible. Prices are whatever the storefront shows a visitor from the run's location, which is usually the store's main market.

### Run it locally

```sh
npm install
npx apify run --input-file examples/input.json
npm run check
```

# Actor input Schema

## `stores` (type: `array`):

Shopify stores to export, as full URLs or bare domains such as allbirds.com. Custom domains work. A collection URL like https://store.com/collections/sale limits that store to the collection.

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

Stop after saving this many products in total, across all stores. Raise it to export whole catalogs.

## `collections` (type: `array`):

Collection handles or collection URLs, such as mens or https://store.com/collections/mens. When set, only products in these collections are saved. Applies to every store in the list.

## `includeVariantAvailability` (type: `boolean`):

Reads each product's /products/<handle>.js page for variant stock status, barcodes and inventory counts where the store shows them. Makes one extra request per product, so very large stores take longer.

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

Walks every collection on the store to fill the collections field. Stores with hundreds of collections can take several minutes before the first product is saved.

## `maxRunMinutes` (type: `integer`):

The run stops cleanly and keeps what it has saved once this much time has passed.

## Actor input object example

```json
{
  "stores": [
    "https://www.allbirds.com"
  ],
  "maxItems": 30,
  "collections": [],
  "includeVariantAvailability": true,
  "includeCollections": false,
  "maxRunMinutes": 60
}
```

# Actor output Schema

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

No description

## `summary` (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 = {
    "stores": [
        "https://www.allbirds.com"
    ],
    "maxItems": 30
};

// Run the Actor and wait for it to finish
const run = await client.actor("omraydev/shopify-store-products").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 = {
    "stores": ["https://www.allbirds.com"],
    "maxItems": 30,
}

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

```

## MCP server setup

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

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/JZLUlKh5eB1T10Tch/builds/2eM4aeQH5zpqgGGGh/openapi.json
