# Shopify Scraper (`dami_studio/shopify-products-scraper`) Actor

Shopify scraper for a store's whole catalogue. Each product row has the title, link, vendor, type, tags, description, lowest and highest price and stock flag, with every variant's SKU, price and compare-at price, plus image URLs. Optional paid sort. $1.00 per 1,000 products.

- **URL**: https://apify.com/dami_studio/shopify-products-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** E-commerce, Integrations, Lead generation
- **Features:** Limited permissions, Multi-tenant, Server interface
- **Stats:** 14 total users, 5 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.97 / 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.
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 scraper for a store's catalogue, one row per product**: title,
handle, link, vendor, product type, tags, description, lowest and highest price, stock flag, and
every variant with its own SKU, price, compare-at price and availability, plus the image URLs.

It reads the public catalogue feed a Shopify store publishes, so there is nothing to apply for and no
key to hold. The limit worth knowing up front: it returns published products only. You get a yes or no on stock, never a number.

| | |
|---|---|
| **Input** | Shopify store URLs or bare domains |
| **Output** | One row per product with variants nested: title, vendor, type, tags, price range, stock flag, SKUs and images |
| **Ceiling** | Whatever the store publishes, capped by `maxItems` per store |
| **Account needed** | None |
| **Price** | $1.00 per 1,000 products on the Free plan, a little less on paid plans. The free plan's $5 a month covers about 5,000. The optional sort adds $0.06 per 1,000 products it reads and leaves out |

### 🔍 What Shopify Scraper does

Paste stores, one per line. A bare domain, a full URL, or any page on the store: each one is reduced
to the store itself and the catalogue is walked from the top, 250 products at a time, until it runs
out or hits your cap.

**Sorting is a paid option.** To sort, the run reads up to 10,000 products of each store, keeps the first ones
by your order, up to `maxItems`, and leaves the rest out. Cheapest and dearest go by the lowest and
highest variant price, newest by when the product was created.

**Optional Notion delivery.** Pick a Notion connector and every product is also written as a page
into your workspace when the run finishes. The dataset is written either way.

### 📋 What data you get from each Shopify product

| What you get | Field |
|---|---|
| The store, product id, title, handle and link | `store`, `productId`, `title`, `handle`, `url` |
| Vendor, product type and the store's own tags | `vendor`, `productType`, `tags` |
| The product copy as plain text | `description` |
| When it was created and published | `createdAt`, `publishedAt` |
| Lowest and highest variant price | `priceMin`, `priceMax` |
| Whether any variant is in stock, and how many variants there are | `available`, `variantCount` |
| Each variant's name, SKU, price, compare-at price and stock flag | `variants` |
| Full-size image URLs | `images` |

### ▶️ How to scrape a Shopify store's products

1. Open [Shopify Scraper](https://apify.com/dami_studio/shopify-products-scraper) and click
   **Try for free**.
2. Paste your stores into **Shopify store URLs**, one per line.
3. Set **Max products per store** to what you actually need.
4. Click **Start**.
5. Download the dataset as JSON, CSV or Excel.

Leave the store box empty and you get one `BAD_INPUT` row, uncharged, telling you so.

### 💰 How much does it cost to scrape Shopify products?

**$1.00 per 1,000 products** on the Free plan. Paid plans pay a little less: $0.99 on Starter, $0.98 on Scale and $0.97 on Business. On the free plan, the $5
Apify gives you each month covers about 5,000 products.

One row is one product, however many variants hang off it. Diagnostic rows are not charged, so a
store that will not answer produces nothing to pay for.

A sort adds one charge of its own: each product it reads and leaves out costs $0.06 per 1,000 on the Free plan, a little less on paid plans.
Without a sort, nothing changes.

One thing to watch: the store list is taken as written. List the same shop twice and it is read
twice and charged twice, so de-duplicate before you paste.

### 📥 What you give it

```json
{
  "storeUrls": ["https://www.gymshark.com", "allbirds.com"],
  "maxItems": 1000
}
```

| Field | Default | What it is |
|---|---|---|
| `storeUrls` | none | Bare domains, full URLs, or any page on the store, one per line. The form opens with two example stores. |
| `maxItems` | `1000` | How many products to take per store before moving on. You are charged per product returned, so this is your spend cap. |
| `sortBy` | none | Paid option: `price-asc` (cheapest first), `price-desc` (dearest first) or `newest`. Empty keeps the store's own order. |
| `notionConnector` | none | Optional. Authorise a Notion connector once under Settings, then pick it here to have the products written into your workspace. |
| `notionParentId` | none | Optional. The Notion data source id to write into. Leave it empty and the pages are created privately in your workspace. |
| `proxyConfiguration` | off | Optional, and not needed for a normal run. Only fill this in if you want the run to go out through servers of your own. |

### 📤 What you get back

A real product row from a run in September 2026. The `tags`, `description` and `images` are cut
short here.

```json
{
  "ok": true,
  "store": "https://gymshark.com",
  "productId": "6806502867146",
  "title": "Gymshark Diffuse Sweat Headwrap - Black",
  "handle": "gymshark-diffuse-sweat-headwrap-headwear",
  "url": "https://gymshark.com/products/gymshark-diffuse-sweat-headwrap-headwear",
  "vendor": "Gymshark | Be a visionary.",
  "productType": "Unisex>Accessories>Headwear>headband",
  "tags": ["accessories", "category:Headwear", "feature:sweat-wicking", "filter-colour:Black", "outlet", "over-50-off", "season:SS26", "..."],
  "description": "A HEADWRAP THAT WORKS\nWe built the Diffuse Sweat Headwrap with the black community and all hair types in mind ...",
  "createdAt": "2026-01-09T04:26:28-08:00",
  "publishedAt": "2026-09-11T10:14:04-07:00",
  "priceMin": 26,
  "priceMax": 26,
  "available": true,
  "variantCount": 1,
  "variants": [
    {"title": "One Size", "sku": "I1C2W-BB2J-OS", "price": 26, "compareAtPrice": null, "available": true}
  ],
  "images": ["https://cdn.shopify.com/s/files/1/0156/6146/files/Diffuse_Sweat_Headwrap_GS_Black_I1C2W_BB2J_6.jpg?v=1789139459", "..."]
}
```

| Field | How to read it |
|---|---|
| `productId`, `handle` | Shopify's own id and URL slug. `productId` is the key to join on between runs. |
| `priceMin`, `priceMax` | The cheapest and dearest variant, as numbers. Equal when there is only one. |
| `compareAtPrice` | Inside `variants`: the crossed-out price, when the store set one. `null` otherwise, which is most of the time. |
| `tags` | The store's own tags, exactly as they set them. Big retailers use these for internal bookkeeping, so expect noise. |
| `createdAt`, `publishedAt` | When the product was made and when it went live. Useful for spotting new arrivals. |
| `images` | Full-size image URLs. The files themselves stay on Shopify. |

### 🧾 Reading the output

| Row | How to spot it | Billed |
|---|---|---|
| A product | `ok` is `true` and it has a `productId` | yes |
| A diagnostic | `ok` is `false` and it carries an `errorCode` | no |

There is no sample row. Filter on `ok == true` and you have your products.

| Code | What it means |
|---|---|
| `BAD_INPUT` | Nothing usable in the store box. |
| `NOT_SHOPIFY` | The catalogue feed did not answer. The row carries `triedHosts` so you can see what was attempted. A store that turned the feed off, or one behind a password, lands here too. |
| `NO_RESULTS` | The store answered with an empty catalogue. |
| `RATE_LIMITED`, `SERVER_ERROR` | The store asked for a slower pace, or its own server erred. Worth one re-run. |
|`NETWORK` | The store would not serve the request, or was unreachable. |
| `NOT_SERVED` | This store wasn't served this run. Re-run it later. Runs before 13 October 2026 wrote `BLOCKED` here, and runs from 13 to 17 October wrote `NETWORK`. |
| `SORT_NOT_LIVE` | A sort was asked for while its price was not in force, so nothing was searched. |
| `TIME_LIMIT` | The run got close to its time limit and stopped early. Every product delivered before that is in your dataset, and the row names the store it was reading and any it never reached. If nothing came back at all, the run still ends as succeeded, with an amber status message. |
| `UNEXPECTED_ERROR` | The run stopped early on an unexpected error. The results before this row are kept. |

### 💡 What people use it for

- Pulling another store's full catalogue with prices and SKUs, then diffing it weekly on `productId`.
- Watching `compareAtPrice` against `price` to see when a store actually discounts.
- Spotting new arrivals by sorting on `publishedAt`, or with `sortBy` set to `newest`.
- Building a supplier price list with variant SKUs already on the row.

From a store's catalogue to restock alerts, in three steps:

1. Run this on the store and keep the `url` of the products you care about.
2. Paste them into **Products to monitor** in
   [Product Price & Stock Monitor](https://apify.com/dami_studio/structured-product-price-stock-monitor),
   one `{"url": ..., "label": ...}` each.
3. Put your Slack or Discord hook into its `webhookUrl` and schedule it.

### 🚧 What it does not do

- **No inventory numbers.** A stock flag, not a quantity. `available` is all
  there is.
- **Published products only.** Drafts, archived products and anything hidden from the storefront are
  not in the feed.
- **A collection URL scrapes the whole store.** Any page you paste is reduced to the store itself, so
  `/collections/sale` returns the full catalogue, not the sale.
- **A store with the feed switched off comes back as `NOT_SHOPIFY`.** Some shops turn it off, and
  password-protected stores answer the same way, so the verdict is not always about the platform.
- **A truncated catalogue can look complete.** If a store stops answering partway through, that store
  ends quietly with the products it had. If a count looks short against what the shop shows, run it
  again.
- **Prices are the store's own currency**, as the feed publishes them. There is no conversion and no
  currency field on the product row.
- **Duplicate store URLs are not merged.** Two entries for one shop means two passes.
- **A sort covers at most 10,000 products per store.** A bigger store is sorted on its first 10,000.
- It is not an official Shopify tool. Dami's Studio is independent and is not affiliated with or endorsed by Shopify, or by any other company named on this page.

### 🧭 Which product scraper do you need?

| If you want | Use |
|---|---|
| Products from a Shopify store's catalogue | This one |
| Which domains run Shopify, with each store's contact details | [Shopify Store Leads](https://apify.com/dami_studio/shopify-store-leads) |
| Restock and price-drop alerts on product pages you name | [Product Price & Stock Monitor](https://apify.com/dami_studio/structured-product-price-stock-monitor) |
| TikTok Shop products with prices and units sold | [TikTok Shop Scraper](https://apify.com/dami_studio/tiktok-shop-scraper) |
| Amazon products by keyword or ASIN, with the on-page reviews | [Amazon Scraper](https://apify.com/dami_studio/amazon-scraper) |
| Export products, MOQ and suppliers on Alibaba.com | [Alibaba.com Scraper](https://apify.com/dami_studio/alibaba-com-scraper) |

### ❓ Questions people ask

#### Do I need a Shopify account or an API key?

No. The catalogue feed is public on every store that leaves it on.

#### Can I scrape just one Shopify collection?

Not here. Whatever page you paste, the whole store is read. Filter on `productType` or `tags`
afterwards.

#### How many products can one run take?

As deep as the store goes. `maxItems` is your own cap, and it is also your spend cap.

#### Why do the tags look like nonsense?

Big retailers put dates, buckets and workflow markers
in there alongside the useful ones.

#### Can I get it into Notion automatically?

Yes. Authorise a Notion connector in your Apify settings and pick it in **Notion connector**. The
dataset is written either way.

#### Can I call it from code or connect it to an AI assistant?

The [API tab](https://apify.com/dami_studio/shopify-products-scraper/api/python) has code for Python, JavaScript and
the CLI. MCP clients like Claude and ChatGPT use
`https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/shopify-products-scraper`; coding assistants can load
[our skill file](https://api.apify.com/v2/key-value-stores/BTBHU3DdiuoF4JOsK/records/apify-ecommerce-SKILL.md). All
three bill your Apify account at the same price.

#### Is scraping a Shopify catalogue legal?

These are public product listings. Apify's write-up on
[the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/) is a good starting
point, and we are not lawyers.

### 🆘 If something breaks

Open the **Issues** tab on the actor page. Send the store URL and the run ID; the `errorCode` and
`triedHosts` on the diagnostic row usually name the problem on their own.

# Actor input Schema

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

One or more Shopify stores to scrape. Accepts a bare domain (shop.com), a full URL (https://shop.com), or any page on the store (https://shop.com/collections/sale), each is normalized to the store origin. Each store's public /products.json endpoint is paginated.

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

Maximum number of products to scrape per store before moving on. Products are paged in batches of 250 until this cap or the catalog is exhausted.

## `sortBy` (type: `string`):

Leave empty to get each store's products in the store's own order. Pick an order and the run reads up to 10,000 products of each store, then returns the first ones by that order, up to Max products per store. Cheapest and dearest go by the lowest and highest variant price, newest by the date the product was created. Each product read and left out is billed as a checked product: see the Pricing tab.

## `notionConnector` (type: `string`):

Optional. Write each product as a page into your Notion when the run finishes. Authorize a Notion connector once in Settings → API & Integrations → MCP connectors, then pick it here. Leave empty to skip (default), results are always saved to the dataset regardless.

## `notionParentId` (type: `string`):

Optional. The Notion data source ID of the database to write into (only used if a Notion connector is set). Leave empty to create the pages privately in your workspace instead.

## `proxyConfiguration` (type: `object`):

Optional, and not needed for a normal run. Your own proxy servers are used exactly as given.

## Actor input object example

```json
{
  "storeUrls": [
    "https://www.gymshark.com",
    "https://www.allbirds.com"
  ],
  "maxItems": 1000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Scraped rows are stored in the default dataset (one row per result). Blocked/empty/error runs return a single uncharged diagnostic row instead.

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

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/shopify-products-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.gymshark.com",
        "https://www.allbirds.com",
    ] }

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/shopify-products-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/phFg3EF6t2dIQjD1E/builds/tBn0mugBS6GfLpFdN/openapi.json
