# Shopify Store Products Scraper (products.json, prices, monitor) (`datahamster/shopify-store-products`) Actor

Scrape every product of any public Shopify store from its own storefront feed: title, vendor, type, tags, description, variants with SKU, price, compare-at price and stock, images and options. Any store URL, no app or API key. Monitor mode alerts on new products, price and stock changes.

- **URL**: https://apify.com/datahamster/shopify-store-products.md
- **Developed by:** [Viktor Dubnytskiy](https://apify.com/datahamster) (community)
- **Categories:** Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.40 / 1,000 result items

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 Store Products Scraper (products.json, prices, monitor)

Point it at any public **Shopify** store — `allbirds.com`, `brand.myshopify.com`, a collection URL — and get the
whole catalogue as flat rows: every product with its variants, prices, compare-at prices, stock flags, images,
options, tags and vendor. The data comes from the storefront feed the shop publishes for its own theme, so it is
the shop's own catalogue, not a rendering of it. No app to install, no Shopify API key, no login.

### What you get

One row per product: `id` (`<host>:<product id>`), `productId`, `url`, `storeUrl`, `storeHost`, `title`,
`handle`, `vendor`, `productType`, `tags`, `description` (plain text, up to 1 000 characters), `price` (cheapest
variant), `priceMax`, `compareAtPrice`, `currency`, `available`, `variantCount`, `variants`
(`id`, `title`, `sku`, `price`, `compareAtPrice`, `available`, `grams`), `imageUrl`, `imageCount`, `options`,
`createdAt`, `updatedAt`, `publishedAt`, `collectionHandle`, `query`, `scrapedAt`.

### Example output

A real row (`storeUrls: ["https://www.allbirds.com"]`), shortened:

```json
{
  "id": "www.allbirds.com:7218356060240",
  "productId": "7218356060240",
  "url": "https://www.allbirds.com/products/mens-strider-explore",
  "storeUrl": "https://www.allbirds.com",
  "storeHost": "www.allbirds.com",
  "title": "Men's Strider Explore - Natural Black (Dark Grey Sole)",
  "handle": "mens-strider-explore",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "description": "From exploring urban landscapes to tackling the trails (or both), this active shoe is here for it all...",
  "price": 130.0,
  "priceMax": 130.0,
  "compareAtPrice": null,
  "currency": "USD",
  "available": false,
  "variantCount": 13,
  "variants": [
    {"id": "41334293889104", "title": "8", "sku": "A11768M080", "price": 130.0, "compareAtPrice": null,
     "available": false, "grams": 919}
  ],
  "imageUrl": "https://cdn.shopify.com/s/files/1/1104/4168/files/...",
  "imageCount": 5,
  "options": ["Size"],
  "publishedAt": "2026-08-26T10:22:14-07:00",
  "updatedAt": "2026-09-16T15:53:47-07:00"
}
```

### Use cases

- **Competitor catalogue and price tracking** — pull a rival's full assortment with per-variant prices and
  compare-at prices, then run it on a schedule in monitor mode and get told the moment something changes.
- **Assortment research** — which vendors, product types and tags a store carries, how deep each product's
  variant tree goes, how much of the catalogue is actually in stock.
- **Dropshipping and reselling** — build a product list with SKUs, weights and images from the supplier's own
  store instead of a stale spreadsheet.
- **Stock alerts** — `availableOnly` plus monitor mode tells you when a sold-out product comes back.
- **Collection snapshots** — scrape only `new-arrivals` or `sale` with `collectionHandles`.

### How it works

1. **Each store's `robots.txt` is read first, at runtime, before any catalogue request.** If the store's own
   `User-agent: *` group forbids `/`, `/products` or `/products.json`, the store is skipped and nothing is
   scraped or charged for it. A collection path a store forbids separately is skipped on its own — some large
   stores publish `Disallow: /collections/*/products*` while leaving the whole-catalogue feed open.
2. The store URL is normalised: `example.com`, `https://example.com/`, `brand.myshopify.com` and a collection
   URL such as `https://example.com/collections/sale` all resolve to the same store (the collection handle is
   picked up from the URL).
3. The catalogue is read page by page (`?limit=250&page=N`). Paging stops on a short page or as soon as a page
   brings no product id that was not already seen — never on "the page was empty", because a store that
   redirects past the end of its catalogue serves the last page again instead.
4. The store home page is read **once per store** for `Shopify.currency`, so rows carry the shop's own currency.
   A theme that does not publish it leaves `currency` as `null` — the actor does not guess a currency from the
   domain or the exit country.
5. `availableOnly`, `minPrice` and `maxPrice` are applied before a row is pushed, so you are not charged for
   rows you filtered out.

### Input

| Field | Meaning | Default |
|---|---|---|
| `storeUrls` | Stores to scrape, one per line. A bare domain, a full URL or a collection URL | required |
| `collectionHandles` | Scrape only these collections, applied to every store | empty (whole catalogue) |
| `maxProductsPerStore` | Stop after this many products per store | `250` |
| `availableOnly` | Keep only products with at least one variant in stock | `false` |
| `includeVariants` | Include the per-variant array (`variantCount` and prices are kept either way) | `true` |
| `minPrice` / `maxPrice` | Keep only products whose cheapest variant is inside this range | empty |
| `tier` | Proxy tier: `auto` climbs none → datacenter → residential only when a store refuses | `auto` |
| `proxyCountry` | Exit country for the proxied tiers | `US` |
| `maxItems` | Stop after this many rows in total | `500` |
| `mode` | `scrape` or `monitor` (only new/changed since the last run) | `scrape` |
| `monitorStateId`, `webhookUrl`, `telegramBotToken`, `telegramChatId` | Monitor-mode state key and alert targets | empty |

### Monitor mode

`mode: monitor` compares this run against the previous run of the same task and returns only what is new or
changed. A product counts as changed when its price, stock flag, variant count or `updatedAt` moves, so you get
price drops, restocks, sell-outs and new products — and nothing else. Alerts can go to a webhook and to Telegram.

### Pricing

| Event | Price |
|---|---|
| result | $0.0005 per product ($0.50 per 1,000) |
| monitor-check | $0.005 per monitor run |
| change | $0.0005 per new or changed product |

You are charged only for rows actually pushed. Most stores need no proxy at all, so platform usage stays small.

### Limits

- **Public storefronts only.** A store whose front end is behind the Shopify password page answers with HTTP
  401; it is reported as `password_protected` and produces no rows. There is no way in.
- **The store decides.** A store whose `robots.txt` forbids the feed is skipped. This is checked per store at
  runtime and cannot be turned off.
- **Some stores block the endpoint at their CDN.** Gymshark, for example, answers `/products.json` with a
  CloudFront 403 on every proxy tier. Such a run is reported as `blocked`, never as "no results".
- **Not every host is a Shopify store.** A host with no product feed is reported as `not_shopify` rather than
  silently returning nothing.
- `currency` is `null` when the store's theme does not publish `Shopify.currency` on its home page. Prices are
  always the numbers the store's own feed returns.
- The feed carries the shop's published catalogue: products hidden from the online-store sales channel, draft
  products and per-customer prices are not in it, and neither are inventory counts (Shopify publishes a boolean
  `available` per variant, not a quantity).
- `description` is the product description as plain text, capped at 1 000 characters.
- Shopify caps a page at 250 products; larger catalogues are paged automatically.

### What is NOT collected

No personal data of any kind. The storefront feed is a product catalogue — it contains no customer, order,
review-author or contact object, and the output schema has no field for one. No e-mail addresses, no names, no
buyer data. Stores are businesses, and only business/catalogue facts are returned.

### FAQ

**Does it need a Shopify API key or a private app?** No. Every request is a plain public web request to the
store's own storefront feed.

**How do I know a URL is a Shopify store?** Just give it — a host that serves no product feed comes back as
`not_shopify` in the run summary, and costs one request.

**Can I scrape only one collection?** Yes: put its handle in `collectionHandles`, or paste the collection URL
straight into `storeUrls`.

**What happens when a run returns nothing?** No rows are pushed and no result events are charged. The
`RUN_SUMMARY` record in the run's key-value store carries `emptyReason`, and `ADAPTER_STATS` carries a per-store
report (`ok`, `robots_disallowed`, `not_shopify`, `password_protected`, `blocked`) so you can tell an empty
catalogue from a refusal.

### Changelog

- 0.1: initial release — whole-catalogue and per-collection scraping, variants with SKU and compare-at prices,
  currency from the store's own page, per-store runtime robots.txt check, price and availability filters,
  monitor mode.

***

If this actor saved you time, a short review on its Store page genuinely helps other people find it. Found a bug
or need a field that is missing? Open a ticket on the **Issues** tab.

# Actor input Schema

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

Stop after this many results (you are charged only for pushed items)

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

scrape = full results; monitor = only new/changed items since the previous run of this task

## `monitorStateId` (type: `string`):

Optional state id when not running as a saved task (monitor mode)

## `webhookUrl` (type: `string`):

POST a change summary here in monitor mode

## `telegramBotToken` (type: `string`):

Optional: bot token for monitor-mode change summaries

## `telegramChatId` (type: `string`):

Optional: chat id that receives monitor-mode summaries

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

Stores to scrape, one per line. A bare domain ("allbirds.com"), a full URL ("https://www.allbirds.com"), a myshopify domain ("brand.myshopify.com") or a collection URL ("https://www.allbirds.com/collections/mens") all work.

## `collectionHandles` (type: `array`):

Scrape only these collections instead of the whole catalogue, e.g. "sale", "new-arrivals", "mens". The handle is the last part of a collection URL. Applied to every store in the list. Leave empty for the full catalogue.

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

Stop after this many products from each store, e.g. 250. Shopify serves at most 250 products per request, so a larger number simply means more pages. Range 1-15000.

## `availableOnly` (type: `boolean`):

Keep only products with at least one variant in stock.

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

Include the per-variant array (id, title, SKU, price, compare-at price, availability, weight). Turn off for a lighter dataset - variantCount and the product prices are returned either way.

## `minPrice` (type: `integer`):

Keep only products whose cheapest variant costs at least this much, in the store's own currency. Leave empty for no lower bound.

## `maxPrice` (type: `integer`):

Keep only products whose cheapest variant costs at most this much, in the store's own currency. Leave empty for no upper bound.

## `tier` (type: `string`):

Most stores serve the feed with no proxy at all. Auto starts without one and climbs only when a store refuses; pin a tier to keep the cost predictable.

## `proxyCountry` (type: `string`):

Two-letter exit country for the proxied tiers, e.g. "US", "DE". Prices come from the store's own feed in its own currency; this only chooses where the requests leave from.

## Actor input object example

```json
{
  "maxItems": 500,
  "mode": "scrape",
  "storeUrls": [
    "https://www.allbirds.com"
  ],
  "maxProductsPerStore": 250,
  "availableOnly": false,
  "includeVariants": true,
  "tier": "auto"
}
```

# Actor output Schema

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

All pushed rows (dataset, JSON)

## `resultsTable` (type: `string`):

Dataset in the Console viewer

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

RUN\_SUMMARY record

# 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": 250
};

// Run the Actor and wait for it to finish
const run = await client.actor("datahamster/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 = {
    "storeUrls": ["https://www.allbirds.com"],
    "maxProductsPerStore": 250,
}

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

```

## MCP server setup

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