# Shopify Store Product Scraper (`praise-most-high/shopify-store-product-catalog`) Actor

Shopify Store Product Scraper reads any storefront's own products.json and returns one row per product: title, vendor, type, tags, price range, compare-at price, sale flag, stock, variants and images. No API key. onlyNewSinceLastRun defaults ON, so a repeat run bills only for changes.

- **URL**: https://apify.com/praise-most-high/shopify-store-product-catalog.md
- **Developed by:** [angel nguyen](https://apify.com/praise-most-high) (community)
- **Categories:** E-commerce, Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 product returneds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

Shopify Store Product Scraper extracts the full public catalogue of any Shopify storefront and returns one flat, typed row per product — title, vendor, product type, tags, price range, compare-at price, sale flag, stock, every variant with its SKU, images and description text. You give it a domain; you get back a spreadsheet of that store's products.

<img src="https://apify-image-uploads-prod.s3.us-east-1.amazonaws.com/0ezaunoiVKF5LFUup-actor-7Hi9eGpdSmB9csbIJ-Lhe0kepLC7-shopify-catalog.png" alt="Shopify Store Product Scraper" width="96" height="96">

Every Shopify storefront publishes its own catalogue at `/products.json`. It is a first-party endpoint that Shopify serves by design, it is the same document the storefront's own JavaScript reads, and it needs no API key, no app install, no partner account and no login. This Actor reads that endpoint, pages it properly, and normalises what comes back.

### Key features

| Feature | Detail |
|---|---|
| Input | one or more storefront domains — `allbirds.com` or any URL on the store |
| Coverage | the whole published catalogue, paged 250 at a time up to your cap |
| Variants | every variant with id, SKU, price, compare-at price, availability and weight |
| Derived pricing | `priceMin`, `priceMax`, `compareAtMax` and an `onSale` flag computed from the variants |
| Derived stock | `variantsInStock` and an `inStock` boolean, from Shopify's own per-variant `available` |
| Incremental runs | `onlyNewSinceLastRun` is **on by default** — a repeat run bills only for products that changed |
| Pre-charge filters | product type and in-stock filters run *before* billing, so filtering lowers your cost |
| Honest failures | a domain that is not a Shopify store is `unavailable` with a reason, and is never charged |
| Output | one row per product, flat and typed — JSON, CSV, Excel, XML |

### Shopify store product scraper: how it works

1. **Normalise the domain.** `allbirds.com`, `https://allbirds.com` and `https://allbirds.com/collections/mens` all resolve to the same storefront. Anything that is not a usable host is reported as unavailable rather than silently skipped.
2. **Page the catalogue.** The Actor requests `/products.json?limit=250&page=N` until a page comes back short or empty, until your `maxProductsPerStore` cap is reached, or until an internal 40-page ceiling. Whenever a cap actually truncated a store, it says so in the log — a cap hit silently would read as "that is the whole catalogue".
3. **Check that this is really a Shopify store.** A domain that is not one answers `/products.json` with its own ordinary HTML page, and HTTP 200. The Actor requires a parseable JSON body with a `products` array before it treats a response as a catalogue. This distinction matters more than it sounds: without it, "this is not a Shopify store" and "this store has no products" collapse into the same empty result, and the second one is a claim about the merchant.
4. **Filter, then charge, then deliver.** Filters run first so you are never billed for rows a filter then discards. Charges are applied before records are handed over and delivery is truncated to whatever your budget actually allowed, so the rows you receive always equal the rows you paid for.

### Tutorial

1. Open the Actor and put one or more storefront domains into **Shopify store domains**. The prefilled example reads `allbirds.com` and `www.kith.com`.
2. Set **Max products per store**. Start at the default 100 while you look at the shape of the output; raise it when you know you want the whole catalogue.
3. Leave **Only products that are new or changed since your last run** switched on. It is on by default and it is what makes a scheduled run cheap.
4. Click **Start**. A single store of 100 products finishes in a few seconds.
5. Open the **Storage** tab and export to CSV or Excel, or read the same rows from the dataset API.
6. To keep it current, open **Schedules** and run it daily. With the incremental setting on, each subsequent run returns only the products whose Shopify `updated_at` has moved.

### Input

The minimum input is a single domain. Everything else has a working default.

```json
{
  "storeDomains": ["allbirds.com", "www.kith.com"],
  "maxProductsPerStore": 100,
  "onlyNewSinceLastRun": true,
  "includeOutOfStock": true,
  "productType": "",
  "includeVariants": true
}
```

| Field | Type | Default | Meaning |
|---|---|---|---|
| `storeDomains` | array of string | — | Storefront domains or any URL on the store. Required. |
| `maxProductsPerStore` | integer | `100` | Hard cap per store. Bounds runtime and spend. |
| `onlyNewSinceLastRun` | boolean | **`true`** | Return only products whose Shopify `updated_at` moved since your last run. **On by default.** |
| `includeOutOfStock` | boolean | `true` | Keep products with no available variant. Turning this **off** filters before charging, so it lowers your bill. |
| `productType` | string | *(empty)* | Keep only this Shopify `product_type`. Also filters before charging. |
| `includeVariants` | boolean | `true` | Attach the full variant array. **Does not change what you are charged** — billing is one event per product either way. |

### Output

One row per product. The block below is a row this Actor actually wrote, copied byte for byte out of the dataset of run `JAle7TcRM2KTZwbRJ` (2026-09-04), whose input was `{"storeDomains":["allbirds.com"],"maxProductsPerStore":1,"onlyNewSinceLastRun":false,"includeOutOfStock":true,"includeVariants":false}`. Nothing in it is edited, elided or illustrative.

```json
{
  "storeDomain": "allbirds.com",
  "productId": 7369944137808,
  "title": "Free Returns Coverage",
  "handle": "free-returns-coverage",
  "url": "https://allbirds.com/products/free-returns-coverage",
  "vendor": "re:do",
  "productType": "return,package_protection",
  "tags": [
    "exclude_rebuy",
    "klaviyo_reviews_exclude",
    "NOREVIEWS",
    "redo-package-protection"
  ],
  "publishedAt": "2026-09-02T11:33:11-07:00",
  "createdAt": "2026-06-05T07:14:57-07:00",
  "updatedAt": "2026-09-03T19:41:44-07:00",
  "descriptionText": null,
  "priceMin": 0.8,
  "priceMax": 2.98,
  "compareAtMax": 2.98,
  "onSale": true,
  "variantCount": 2,
  "variantsInStock": 2,
  "inStock": true,
  "imageCount": 1,
  "featuredImage": "https://cdn.shopify.com/s/files/1/1104/4168/files/Screenshot_2026-06-11_at_10.12.01_AM.png?v=1781197934",
  "options": [
    "Price"
  ],
  "scrapedAt": "2026-09-04T02:41:44.455Z"
}
```

That run had `includeVariants` switched off, so its rows carry no variant array. With the setting on — which is the default — every row also carries `variants`. This is the first of the thirteen variants on `mens-strider-explore`, copied out of the dataset of run `fd7s6mPslZ2FcfHjo` (2026-09-03, input `{"storeDomains":["allbirds.com"],"maxProductsPerStore":5,"onlyNewSinceLastRun":false,"includeOutOfStock":true,"includeVariants":true}`):

```json
{
  "variantId": 41334293889104,
  "title": "8",
  "sku": "A11768M080",
  "price": 130,
  "priceRaw": "130.00",
  "compareAtPrice": null,
  "available": false,
  "requiresShipping": true,
  "grams": 919
}
```

Three fields above are **derived**, not copied. `onSale` is true only when a compare-at price actually exceeds the lowest live price — it reads `true` on the product row because `compareAtMax` 2.98 is greater than `priceMin` 0.8. `inStock` is the OR over Shopify's own per-variant `available` flag, so a product every one of whose variants is sold out reads `false` rather than being dropped. `priceRaw` keeps the exact decimal string Shopify published, so you can reconcile against a merchant's own figures without a float rounding in the way.

### Pricing

This Actor is priced per event, so you pay for what it delivers rather than for how long it runs.

| Event | Price | Charged when |
|---|---|---|
| `product-returned` | $0.004 | Once per product row written to the dataset |

There is no per-run start fee, no monthly minimum, and no charge for platform usage beyond Apify's own. A run that returns nothing costs nothing at all.

#### What a repeat run actually costs, worked through

This is the number that matters, and it is the reason `onlyNewSinceLastRun` defaults to on. Take one storefront with a 500-product catalogue, polled daily for a month, where roughly 20 products change on a given day:

| Setting | Products billed per day | Monthly cost |
|---|---|---|
| `onlyNewSinceLastRun: true` *(the default)* | 500 on day one, ~20 a day after | **$2.00 + $2.32 = $4.32** |
| `onlyNewSinceLastRun: false` | 500 every day | **$60.00** |

That is a **14x** difference, and every dollar of the difference would have been spent re-buying products you already had. The default is the cheap one on purpose. Turn it off when you genuinely want a full re-snapshot — for a one-off export, or to rebuild a catalogue you have lost — and not by accident.

The other two cost levers work the same way and also default to the cheaper outcome where the cheaper outcome is the sensible one:

- **`productType`** filters before charging. Reading only `"Shoes"` from a 500-product store bills for the shoes.
- **`includeOutOfStock: false`** filters before charging, so sold-out products cost you nothing.
- **`includeVariants`** is *not* a cost lever, and this README will not pretend it is one. Billing is one event per product whether the variant array is attached or not. It defaults to on because the variants are usually the point.

### Frequently asked questions

**Is `/products.json` a public endpoint?**
Yes. It is a standard part of every Shopify storefront and is served without authentication by design. This Actor accesses nothing behind a login and holds no credentials for any store.

**Does it work on every Shopify store?**
It works on any storefront that serves `/products.json`, which is the default. A merchant can disable it, and a small number do. When that happens you get an `unavailable` row naming the reason, and you are not charged.

**What happens if I give it a domain that is not a Shopify store?**
You get `unavailable` with the reason "the endpoint returned HTML, not JSON — this domain does not serve a Shopify products.json", and **no charge**. That is deliberate: an unresolvable input must never bill.

**Does it return inventory levels?**
No, and nothing here implies otherwise. `/products.json` publishes a per-variant `available` boolean, not a quantity. You get `inStock` and `variantsInStock`; you do not get "17 left", because the endpoint does not carry it.

**Does it return customer, order or revenue data?**
No. None of that appears on a public storefront endpoint and this Actor makes no attempt to reach it.

**How far back does the catalogue go?**
`/products.json` publishes the store's currently listed products. It is a catalogue, not an archive — a product the merchant has delisted is gone from it, and this Actor cannot recover one.

**Can I use it to track a competitor's price changes?**
That is the common use. Schedule it daily with the incremental setting on; every row you receive is a product whose `updated_at` moved, and `priceMin` / `compareAtMax` / `onSale` give you the price story per product.

**What if my budget cap is reached mid-run?**
Delivery is truncated to the number of records actually charged and the run says so in the log. You are never billed for rows you did not receive.

### Limits and troubleshooting

- **A store returns fewer products than you expected.** Check `maxProductsPerStore` first — the log names any store that a cap truncated. Beyond that, `/products.json` excludes products the merchant has not published to the online-store channel.
- **A 40-page ceiling.** The Actor stops at 10,000 products per storefront and says so in the log rather than paging indefinitely. Split a larger catalogue with `productType`.
- **Rate limiting.** Storefronts throttle aggressive paging. The Actor backs off and retries up to three times, then reports the store as unavailable. An unreachable store is reported, never silently treated as an empty one.
- **A store answers 200 with HTML.** That is the "not a Shopify store" case, and it is reported as such rather than as an empty catalogue.

### Integrations and API

The Actor is callable from anywhere the Apify API reaches.

```bash
curl -X POST "https://api.apify.com/v2/acts/praise-most-high~shopify-store-product-catalog/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"storeDomains":["allbirds.com"],"maxProductsPerStore":50}'
```

- **Scheduling** — run it daily from the Apify scheduler; with the incremental default on, each run is a change feed.
- **Webhooks** — fire on `ACTOR.RUN.SUCCEEDED` to push new rows into your own warehouse.
- **Python and JavaScript SDKs** — both call it the same way; see the [Apify client docs](https://docs.apify.com/api/client/js/).
- **MCP** — reachable as a tool from any MCP-speaking agent through Apify's own MCP server.

### Related Actors

Other Actors on this account that pair with this one:

- [App Store Price & Rating Monitor](https://apify.com/praise-most-high/app-store-intelligence) - price and rating monitoring on a different marketplace
- [Overture POI Search - Austin Places](https://apify.com/praise-most-high/local-poi-open) - find the local merchant, then read its catalogue
- [Dexscreener Token Price Monitor](https://apify.com/praise-most-high/dex-token-price-monitor) - the same change-only price monitoring pattern
- [Skool Community Stats Scraper](https://apify.com/praise-most-high/skool-community-stats-scraper) - creator commerce - the same buyer measures storefronts and paid communities together

### Legal and data handling

> **Unofficial.** This Actor is not affiliated with, endorsed by, or sponsored by Shopify Inc., Allbirds, Kith, or any merchant whose storefront you point it at. "Shopify" is a trademark of Shopify Inc. and is used here only to identify the storefront software being read.

This Actor reads a public, unauthenticated storefront endpoint that Shopify serves by design, and returns product catalogue data — titles, prices, variants, images and descriptions. It collects **no personal data**, no customer records, no order data and nothing behind an authentication boundary.

Product descriptions, images and trade dress belong to the merchant who published them. This Actor retrieves them; it does not license them to you. Whether your intended use of that output is permitted is a question about your jurisdiction, your purpose and the merchant's own terms, and it is yours to answer — this Actor makes no representation about it.

### Support and feedback

Open an issue on the Actor's Issues tab. Bug reports that include the run ID and the input that produced the problem are answered fastest. Feature requests are read and are the main source of what gets built next.

# Actor input Schema

## `storeDomains` (type: `array`):

One or more Shopify storefront domains. A bare domain (allbirds.com) or any URL on the store (https://allbirds.com/collections/mens) both work — the host is taken and the rest ignored. A domain that does not serve a Shopify products.json is reported as unavailable with the reason, and is not charged.

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

Hard cap on products read per storefront. Bounds both runtime and spend. A store with fewer products returns fewer rows and costs less. Raise it to walk a large catalogue; the Actor tells you in the log whenever a cap actually truncated a store.

## `onlyNewSinceLastRun` (type: `boolean`):

ON BY DEFAULT, and leaving it on is what keeps a repeat run cheap. Shopify publishes an updated\_at timestamp per product, so a scheduled run returns and bills for only the products whose timestamp has moved since you last received them. Turn it off only when you deliberately want a full re-snapshot of the whole catalogue — that costs the full per-product price again for products you already have.

## `includeOutOfStock` (type: `boolean`):

Keep products whose every variant is sold out. Turning this OFF filters them out before charging, so you are not billed for rows you did not want. Leave it on for a complete catalogue.

## `productType` (type: `string`):

Keep only products whose Shopify product\_type matches this exactly, case-insensitively. Leave empty for every type. The filter runs BEFORE charging, so a narrow filter lowers the bill rather than raising it.

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

Attach every variant — id, SKU, price, compare-at price, availability, weight — to each product row. This does NOT change what you are charged: billing is one event per product either way. Turn it off only if you want narrower rows.

## Actor input object example

```json
{
  "storeDomains": [
    "allbirds.com",
    "www.kith.com"
  ],
  "maxProductsPerStore": 100,
  "onlyNewSinceLastRun": true,
  "includeOutOfStock": true,
  "includeVariants": true
}
```

# Actor output Schema

## `products` (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 = {
    "storeDomains": [
        "allbirds.com",
        "www.kith.com"
    ],
    "maxProductsPerStore": 100,
    "onlyNewSinceLastRun": true,
    "includeOutOfStock": true,
    "productType": "",
    "includeVariants": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("praise-most-high/shopify-store-product-catalog").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 = {
    "storeDomains": [
        "allbirds.com",
        "www.kith.com",
    ],
    "maxProductsPerStore": 100,
    "onlyNewSinceLastRun": True,
    "includeOutOfStock": True,
    "productType": "",
    "includeVariants": True,
}

# Run the Actor and wait for it to finish
run = client.actor("praise-most-high/shopify-store-product-catalog").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 '{
  "storeDomains": [
    "allbirds.com",
    "www.kith.com"
  ],
  "maxProductsPerStore": 100,
  "onlyNewSinceLastRun": true,
  "includeOutOfStock": true,
  "productType": "",
  "includeVariants": true
}' |
apify call praise-most-high/shopify-store-product-catalog --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,praise-most-high/shopify-store-product-catalog"
        }
    }
}

```

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/7Hi9eGpdSmB9csbIJ/builds/ZZw468Beueid6xytx/openapi.json
