> ## Documentation Index
> Fetch the complete documentation index at: https://rocketyard.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These Actors run on the Apify platform and charge per result. Call one with POST https://api.apify.com/v2/acts/rocketyard~<actor-id>/run-sync-get-dataset-items and the header Authorization: Bearer <APIFY_TOKEN>, or connect Apify's MCP server at https://mcp.apify.com?tools=rocketyard/<actor-id>.

# Shopify Products API & Scraper

> $0.50 per 1,000 products, no subscription. Shopify products API: title, vendor, tags, price range, discount, currency and more as JSON or CSV, no login.

Rocketyard's Shopify Products API & Scraper exports the whole public catalog of a Shopify store and returns one clean record per product: store, title, handle, product id, description, vendor and more. It runs on the Apify platform, so you can start it from the Apify Console, the REST API, an AI agent or a no-code tool.

> [card] [Run it on Apify](https://apify.com/rocketyard/shopify-products-scraper)
>
> \$0.50 per 1,000 products, no subscription. You run it with your own Apify account, and Apify handles billing.

## Who it's for

* E-commerce brands and agencies watching competitor catalogs and prices
* Developers of price trackers, product feeds and comparison sites
* Analysts building e-commerce market datasets
* AI developers who need product data as a tool or in a pipeline

## What you can do with it

* Export a Shopify store's whole catalog to a spreadsheet or database
* Track prices, discounts and sold-out variants on many stores every day
* Pull only the products that changed since the last run

## How it works

Stores on Shopify's standard storefront publish their catalog as JSON at `/products.json` and `/collections/<handle>/products.json`, with no login or API key. The Actor reads those pages over plain HTTP, 250 products per request and one request per second, until the catalog ends or your limit is reached. It honours each store's robots.txt and crawl-delay, waits and retries a bounded number of times when a store asks it to slow down, and turns the JSON into typed records: prices as numbers, dates in ISO 8601, descriptions as clean text. The currency comes from the store's own answer for those prices. A store that refuses, or that isn't on Shopify, gets an error item that says why, and isn't charged.

## Example output

A real record from a test run on tentree.com on 2026-11-05. Fields the page didn't publish are left out here; the Actor returns them as `null`.

```json theme={null}
{
  "url": "https://tentree.com/products/slogan-peak-hat-deep-olive-i-like-your-dog",
  "type": "Product",
  "store": "tentree.com",
  "productId": 8143518761146,
  "title": "Slogan Peak Hat",
  "handle": "slogan-peak-hat-deep-olive-i-like-your-dog",
  "description": "Show them what's on your mind with these fun, embroidered caps — each of which is made using organic cotton that's super soft and good for our planet.",
  "vendor": "tentree",
  "productType": "Accessories",
  "tags": [
    "accessories",
    "baseball-cap",
    "best-sellers",
    "dirty-deals",
    "final-sale",
    "gh-unisex-slogan-peak-hat-meteorite-black-overthinking",
    "gift-guide-25-all",
    "gifts-best-sellers-26"
  ],
  "options": [
    {
      "name": "Color",
      "values": [
        "DEEP OLIVE I LIKE YOUR DOG"
      ]
    },
    {
      "name": "Size",
      "values": [
        "ONE"
      ]
    }
  ],
  "priceMin": 36,
  "priceMax": 36,
  "compareAtPrice": 45,
  "onSale": true,
  "discountPercent": 20,
  "currency": "USD",
  "available": true,
  "variantCount": 1,
  "availableVariantCount": 1,
  "variants": [
    {
      "id": 44904928411834,
      "sku": "TAU5925-5831-ONE",
      "title": "DEEP OLIVE I LIKE YOUR DOG / ONE",
      "options": {
        "Color": "DEEP OLIVE I LIKE YOUR DOG",
        "Size": "ONE"
      },
      "price": 36,
      "compareAtPrice": 45,
      "available": true,
      "weightGrams": 300,
      "requiresShipping": true,
      "taxable": true,
      "image": "https://cdn.shopify.com/s/files/1/2341/3995/files/Green-Organic-Cotton-Low-Profile-Embroidered-Baseball-Cap-TAU5925-5831_1.jpg?v=1749230208",
      "updatedAt": "2026-11-05T18:07:35.000Z"
    }
  ],
  "image": "https://cdn.shopify.com/s/files/1/2341/3995/files/Green-Organic-Cotton-Low-Profile-Embroidered-Baseball-Cap-TAU5925-5831_1.jpg?v=1749230208",
  "images": [
    {
      "url": "https://cdn.shopify.com/s/files/1/2341/3995/files/Green-Organic-Cotton-Low-Profile-Embroidered-Baseball-Cap-TAU5925-5831_1.jpg?v=1749230208",
      "width": 960,
      "height": 1200,
      "variantIds": [
        44904928411834
      ]
    },
    {
      "url": "https://cdn.shopify.com/s/files/1/2341/3995/files/Green-Organic-Cotton-Low-Profile-Embroidered-Baseball-Cap-TAU5925-5831_3.jpg?v=1749230208",
      "width": 960,
      "height": 1200,
      "variantIds": []
    }
  ],
  "publishedAt": "2026-10-28T02:51:19.000Z",
  "createdAt": "2025-06-06T17:16:44.000Z",
  "updatedAt": "2026-11-05T18:07:35.000Z",
  "sourceUrl": "https://tentree.com",
  "scrapedAt": "2026-11-05T18:07:35.372Z"
}
```

## Fields

| Field | Description | Filled in latest test |
| - | - | - |
| `store` | Store domain, without `www.` | 100% |
| `title` | Product title | 100% |
| `handle` | The product's URL name in the store | 100% |
| `productId` | Shopify product ID | 100% |
| `description` | Description as clean text, with HTML removed | 100% |
| `vendor` | Vendor or brand, as the store enters it | 100% |
| `productType` | Product type, as the store enters it | 100% |
| `tags` | Tags, as a list | 100% |
| `options` | Option names and their values, such as Size: S, M, L. Empty for products with no options | 73% |
| `priceMin` | Lowest variant price, as a number | 100% |
| `priceMax` | Highest variant price, as a number | 100% |
| `compareAtPrice` | Earlier price of the most discounted variant, only when it is above that variant's price. Variant records: this variant's | 9% |
| `onSale` | True when a variant has an earlier price above its price. Variant records: this variant | 100% |
| `discountPercent` | Discount of the most discounted variant, in percent, worked out from the two prices. Variant records: this variant | 9% |
| `currency` | ISO 4217 code of these prices, as the store named it in the same response. Empty when the store did not name one | 100% |
| `available` | True when a variant can be bought in the market read, false when none can. A flag, not a stock count. Variant records: this variant | 100% |
| `variantCount` | Number of variants | 100% |
| `availableVariantCount` | Number of variants that can be bought | 100% |
| `variants` | Every variant: `id`, `sku`, `title`, `options` (name to value), `price`, `compareAtPrice`, `available`, `weightGrams`, `requiresShipping`, `taxable`, `image`, `updatedAt`. Left out of variant records | 100% |
| `image` | Main image URL. Variant records: the image of this variant when it has one | 100% |
| `images` | Every image: `url`, `width`, `height`, `variantIds` | 100% |
| `publishedAt` | Published (ISO 8601, UTC) | 100% |
| `createdAt` | Created (ISO 8601, UTC) | 100% |
| `updatedAt` | Last updated (ISO 8601, UTC). Variant records: this variant's | 100% |
| `sourceUrl` | The store or collection the record was read from | 100% |
| `variantId` | Variant records only: Shopify variant ID | 0% |
| `variantTitle` | Variant records only: variant title, such as "Black / M". Empty for products with no options | 0% |
| `sku` | Variant records only: SKU, when the store sets one | 0% |
| `selectedOptions` | Variant records only: this variant's options, such as \{"Size": "M"} | 0% |
| `price` | Variant records only: this variant's price, as a number | 0% |
| `weightGrams` | Variant records only: weight in grams, when the store sets one | 0% |
| `requiresShipping` | Variant records only: whether it ships | 0% |
| `taxable` | Variant records only: whether it is taxed | 0% |
| `url` | the page the record came from | 100% |
| `scrapedAt` | ISO timestamp of the extraction | 100% |

A field is filled when the source publishes it. The Actor never guesses a value: a missing field comes back as `null`. "Filled in latest test" is the share of records from the latest test run (tentree.com) that had the field; other sources publish more or less.

## Pricing

You pay $0.50 per 1,000 products, plus a start fee of $0.00005 per run. Products removed by your filters are not charged, and neither are inputs that fail. Each stored record is one result, so with **Variant** records each variant counts.

| Products | Cost |
| - | - |
| 1,000 | \$0.50 |
| 10,000 | \$5.00 |
| 100,000 | \$50.00 |

Set `maxItems` or the run's **Maximum cost per run** in Apify to cap spending.

## API

Every Apify Actor is also a REST endpoint, so Shopify Products API & Scraper works as a Shopify products API, a Shopify product data API, a Shopify catalog API or a Shopify scraper API. Call it from any language with the Apify REST API, or with Apify's JavaScript and Python clients. You need an Apify API token.

### Shopify products API request

```bash theme={null}
curl -s -X POST "https://api.apify.com/v2/acts/rocketyard~shopify-products-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"startUrls":[{"url":"https://muji.us"}],"stores":["deathwishcoffee.com"],"country":"US","inStockOnly":true,"maxItems":1000}'
```

```js theme={null}
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('rocketyard/shopify-products-scraper').call({"startUrls":[{"url":"https://muji.us"}],"stores":["deathwishcoffee.com"],"country":"US","inStockOnly":true,"maxItems":1000});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python theme={null}
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("rocketyard/shopify-products-scraper").call(run_input={"startUrls":[{"url":"https://muji.us"}],"stores":["deathwishcoffee.com"],"country":"US","inStockOnly":true,"maxItems":1000})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

The synchronous endpoint returns the records directly. For long crawls, start the run asynchronously and read the dataset when it finishes.

## MCP

Apify's MCP server exposes this Actor as a tool to Claude, Cursor and other MCP clients. [Open this Actor's MCP setup page](https://mcp.apify.com?tools=rocketyard/shopify-products-scraper) for setup steps per client, or connect it with this URL:

```text theme={null}
https://mcp.apify.com?tools=rocketyard/shopify-products-scraper
```

Claude Code:

```bash theme={null}
claude mcp add --transport http apify "https://mcp.apify.com?tools=rocketyard/shopify-products-scraper"
```

Cursor (`.cursor/mcp.json`) and other clients that read an `mcpServers` config:

```json theme={null}
{
  "mcpServers": {
    "apify": { "url": "https://mcp.apify.com?tools=rocketyard/shopify-products-scraper" }
  }
}
```

On first use your browser opens so you can sign in to Apify. Then ask the agent, for example: "Export a Shopify store's whole catalog to a spreadsheet or database". The tool returns JSON records, and the runs are billed to that Apify account at \$0.50 per 1,000 products.

## Automation

n8n, Make and Zapier all connect to Apify. Each example below uses Actor `rocketyard/shopify-products-scraper` with this input:

```json theme={null}
{
  "startUrls": [
    {
      "url": "https://muji.us"
    }
  ],
  "stores": [
    "deathwishcoffee.com"
  ],
  "country": "US",
  "inStockOnly": true,
  "maxItems": 1000
}
```

### n8n

Copy this workflow and paste it onto an n8n canvas. It runs the Actor every morning through an HTTP Request node and returns the records as n8n items.

```json theme={null}
{
  "name": "Shopify Products API & Scraper daily",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 7
            }
          ]
        }
      },
      "id": "c1f6a2d0-4a57-4b5e-9d7a-0a1b2c3d4e01",
      "name": "Every day at 07:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/rocketyard~shopify-products-scraper/run-sync-get-dataset-items",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"startUrls\": [\n    {\n      \"url\": \"https://muji.us\"\n    }\n  ],\n  \"stores\": [\n    \"deathwishcoffee.com\"\n  ],\n  \"country\": \"US\",\n  \"inStockOnly\": true,\n  \"maxItems\": 1000\n}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "c1f6a2d0-4a57-4b5e-9d7a-0a1b2c3d4e02",
      "name": "Run Shopify Products API & Scraper",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        260,
        0
      ],
      "credentials": {
        "httpBearerAuth": {
          "name": "Apify API token"
        }
      }
    }
  ],
  "connections": {
    "Every day at 07:00": {
      "main": [
        [
          {
            "node": "Run Shopify Products API & Scraper",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {}
}
```

Then open the HTTP Request node and create an **HTTP Bearer Auth** credential named `Apify API token`, with your token from [Apify Console](https://console.apify.com/settings/integrations) as the bearer token. The token stays in n8n's credential store; the workflow only names the credential. Change the input in the node's JSON body to your own.

The node waits up to 300 seconds, which is also the limit of Apify's synchronous endpoint. An input that yields nothing comes back as an item with an `error` field, which you can drop with a Filter node. Apify also publishes a community node, `@apify/n8n-nodes-apify`, with a **Run an Actor and get dataset** operation.

### Make

1. Add Apify's **Run an Actor** module and connect your Apify account.
2. Choose `rocketyard/shopify-products-scraper`, paste the input above and set **Run synchronously** to **Yes**. Make limits how long a synchronous run can take, depending on your plan. For longer runs, start the scenario from **Watch Actor Runs** instead.
3. Add **Get Dataset Items** with the run's default dataset ID.
4. Send the items wherever you need them, for example with Google Sheets **Add a Row**, mapping `store`, `title`, `handle` and `productId`.

### Zapier

1. Add Apify's **Run Actor** action, connect your Apify account, choose `rocketyard/shopify-products-scraper` and paste the input above.
2. Zapier stops synchronous runs after 30 seconds. For anything larger than a small run, let the run continue in the background and start a second Zap from the **Finished Actor run** trigger.
3. Add **Fetch dataset items** with the run's dataset ID, then map `store`, `title`, `handle` and `productId` into your app.
4. Test with a small input and check that the Zap handles every record before you turn it on.

## Limits

* On 2026-11-05, 11 of 14 well-known stores served their catalog. Two run Shopify behind their own front end or don't publish the feed, and one refused with a bot check.
* No inventory quantities and no barcodes: `/products.json` doesn't publish them. `available` is the store's in-stock flag, not a count.
* Shopify serves one listing up to an offset of 25,000 products (HTTP 400 after page 100). A larger store returns those 25,000 and a free `HTTP_ERROR` item that says so. Add its collection URLs to reach more; a product in several inputs is charged once.
* Sites that run Shopify behind their own front end usually don't serve the feed, and return a free `NO_STRUCTURED_DATA` item, like any site not on Shopify.
* Some stores refuse automated reads or rate-limit them. Those return a free `BLOCKED` item. The Actor never logs in, solves a check or disguises itself, and a proxy will not change a refusal aimed at the feed itself.
* A store selling in several markets answers for one of them, and prices, currency and `available` all follow it. Without **Market country** that is the market of where the run comes from, so set it to compare runs. On 2026-11-05, one store showed 0 of 26 variants available to a visitor in Kazakhstan, and 37 of 51 with `US`.
* A product URL is found by reading its store or collection until the product turns up. The reading is free, but on a large catalog it takes a minute or two.

## FAQ

### How much does it cost?

$0.50 per 1,000 products plus $0.00005 per run, billed by Apify. 10,000 products cost \$5.00. There is no subscription, and you can cap any run with **Maximum cost per run**.

Your Apify plan sets your discount tier (Platinum and Diamond are enterprise tiers):

| Tier | Per 1,000 products |
| - | - |
| Free | \$0.50 |
| Bronze | \$0.50 |
| Silver | \$0.40 |
| Gold | \$0.33 |
| Platinum | \$0.33 |
| Diamond | \$0.33 |

### How do I call it from the API?

One request starts a run and returns its results, using your [Apify API token](https://console.apify.com/settings/integrations):

```bash theme={null}
curl -X POST "https://api.apify.com/v2/acts/rocketyard~shopify-products-scraper/run-sync-get-dataset-items" -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" -d '{"startUrls":[{"url":"https://muji.us"}],"stores":["deathwishcoffee.com"],"country":"US","inStockOnly":true,"maxItems":1000}'
```

### What happens when a URL fails?

The dataset gets one free error item for it, with a code and the reason, and the other inputs in the run carry on. [Error codes](https://rocketyard.dev/errors.md) explains each code, whether a retry helps and what to do.

### Which stores does it work on?

Stores on Shopify's standard storefront, which is most Shopify stores. You can check any store by opening `https://<store>/products.json` in a browser: if you see JSON with a `products` list, the Actor can read it.

### What happens when a field is missing?

The field comes back as `null`. The Actor reports what the source publishes and never fills in guesses.

### Is it allowed to scrape this data?

The Actor reads only the catalog a store publishes for anyone at `/products.json`, honours robots.txt, sends no cookies, and doesn't work around any refusal. The feed holds product data and no customer data. Some stores' terms of service restrict automated access, so check the terms of the stores you read. You are responsible for how you use the data, including copyright in descriptions and photos.

### Can I use it from an AI agent?

Yes. Connect Apify's MCP server as shown in [MCP](#mcp), and the agent can call the Actor as a tool.

## Alternatives

See [how it compares with other shopify product scrapers](https://rocketyard.dev/shopify-products-scraper-alternatives.md) on price, users and run success, from Apify Store data. Need a field or site behaviour that isn't covered? Open an issue on the [Actor page](https://apify.com/rocketyard/shopify-products-scraper).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.