# Shopify Products Scraper + WooCommerce (`sfoln/shopify-products-scraper`)

Scrape every product of any Shopify or WooCommerce store: title, vendor, type, tags, price and compare-at price, discount, stock, variants, images and dates, one row per product or per variant. No login or API key. $0.80 per 1,000 products + $0.001 per store.

- Family: Company & lead intelligence
- Categories: Ecommerce, Lead generation, Automation
- Availability: on request (private on Apify). Email contact@sfoln.com with subject "Access request: Shopify Products Scraper + WooCommerce (sfoln/shopify-products-scraper)" before calling; calls fail until it is public.
- JSON: https://www.sfoln.com/actors/shopify-products-scraper.json
- MCP: https://mcp.apify.com?tools=sfoln/shopify-products-scraper
- REST (sync): POST https://api.apify.com/v2/actors/sfoln~shopify-products-scraper/run-sync-get-dataset-items
- REST (async): POST https://api.apify.com/v2/actors/sfoln~shopify-products-scraper/runs

Scrape every product of any Shopify or WooCommerce store into clean rows: give store URLs and get one row per product (or per variant) with title, vendor, product type, tags, price and compare-at price, discount, stock availability, variants, images and creation / update dates. The platform is detected per store and the catalog is read from the store's public storefront JSON (Shopify /products.json, the WooCommerce Store API), so there is no login, no API key and no browser. Built for e-commerce analysts, competitor price monitoring, dropshippers, agencies, market research and AI agents.

## Price per delivered record

| Event | Price | Per 1,000 | Charged for |
| --- | --- | --- | --- |
| Store product (`product`) | $0.0008 | $0.80 / 1,000 | One product (or, with includeVariants, one variant) delivered as a normalized dataset row with prices, availability, variants and images. |
| Store catalog (`store`) | $0.001 | $1 / 1,000 | One store whose catalog delivered at least one row in this run (platform detection and paging). |

Pay-per-event, USD, billed by Apify; no platform usage charge. Only records with status `ok` are charged. Cap a run with `maxTotalChargeUsd`. Apify free plan: up to 10 per run.

## Run

```bash
curl -X POST "https://api.apify.com/v2/actors/sfoln~shopify-products-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"storeUrls":["https://www.allbirds.com"],"maxProductsPerStore":10}'
```

MCP client configuration:

```json
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/shopify-products-scraper"
    }
  }
}
```

Example prompt: List the 20 newest products of allbirds.com with their prices, compare-at prices and whether they are in stock, then tell me which ones are on sale.

## Input

Add Shopify or WooCommerce store URLs. Each store produces one dataset row per product (or per variant) plus one summary record.

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `storeUrls` (required) | array | — | Store URLs: Store homepages or domains (https://www.allbirds.com, allbirds.com) or Shopify collection URLs (https://store.com/collections/sale, also under a market folder such as /en-ca/collections/sale), one per line. The platform is detected per store: Shopify (/products.json) or WooCommerce (Store API). Duplicates are removed. Accounts on the Apify free plan can process up to 10 stores per run. |
| `maxProductsPerStore` | integer | `1000` | Max products per store: Rows per store (1–50,000): products, or variants with includeVariants. Each delivered row is one charged event. |
| `collections` | array | — | Collections (Shopify): Shopify collection handles or URLs (sale, new-arrivals, https://store.com/collections/sale) to list instead of the whole catalog. Products in several collections are listed once. |
| `includeVariants` | boolean | `false` | One row per variant (Shopify): Deliver one row per variant (size, colour…) with its own SKU, price, compare-at price, availability and image, instead of one row per product. WooCommerce stores are always listed per product. |
| `includeDescription` | boolean | `false` | Include product descriptions: Add the plain-text product description to each product row (up to 20,000 characters). |
| `maxStores` | integer | `1000` | Max stores: Safety cap on the number of stores processed in this run. |
| `concurrency` | integer | `5` | Concurrency: How many stores are processed in parallel (1–10). Pages of one store are read one at a time at up to 2 requests per second. |
| `itemTimeoutSecs` | integer | `600` | Time limit per store (seconds): Paging for one store stops cleanly shortly before this limit (60–3600). |
| `includeRaw` | boolean | `false` | Include raw upstream response: Attach the store's original product JSON to each row (variant rows: the variant's own JSON; larger output). |

Example input:

```json
{
  "storeUrls": [
    "https://www.allbirds.com"
  ],
  "maxProductsPerStore": 10
}
```

## Output

One record per input item with `status` (`ok` is charged; `not_found`, `error`, `skipped_*` are free) and `charged`. The run stores an `OUTPUT` summary record.

| Field | Type | Meaning |
| --- | --- | --- |
| `input` | string | object | Input: The store entry as given. |
| `recordType` | string | Record type: product (one per delivered product, charged product) \| variant (with includeVariants, one per variant, charged product) \| summary (one per store; charged store when at least one row was delivered) |
| `position` | integer | Position: Product and variant rows: position within the store (1-based). |
| `platform` | string | null | Platform: shopify \| woocommerce |
| `store` | string | null | Store: The store's host (after redirects, e.g. www.allbirds.com). |
| `productId` | string | Product ID |
| `handle` | string | null | Handle: URL handle (Shopify) or slug (WooCommerce). |
| `title` | string | Title: Product title (variant rows: the product's title). |
| `url` | string | null | Product URL: Variant rows: the product URL with ?variant=<id> (Shopify). |
| `vendor` | string | null | Vendor: Shopify vendor; WooCommerce brand when the store uses brands. |
| `productType` | string | null | Product type: Shopify product type; WooCommerce: the first category. |
| `tags` | array | Tags |
| `categories` | array | Categories: WooCommerce categories (Shopify products have none in the storefront JSON). |
| `price` | number | null | Price: Product rows: the lowest variant price. Variant rows: the variant's price. |
| `priceMin` | number | null | Lowest price |
| `priceMax` | number | null | Highest price |
| `compareAtPrice` | number | null | Compare-at price: The regular price of discounted variants (highest of them); null when nothing is on sale. |
| `currency` | string | null | Currency: ISO 4217 code (Shopify: from the store's /meta.json). |
| `onSale` | boolean | On sale: At least one variant is below its compare-at price. |
| `discountPct` | integer | null | Discount %: Largest discount among variants, in percent of the compare-at price. |
| `available` | boolean | null | Available: At least one variant can be bought (variant rows: this variant). |
| `variantCount` | integer | Variants |
| `variants` | array | Variants: Product rows: up to 100 { variantId, title, sku, price, compareAtPrice, available, options, grams }. |
| `options` | array | object | Options: Product rows: [{ name, values }]. Variant rows: { optionName: value }. |
| `images` | array | Images: Up to 20 image URLs. |
| `imageCount` | integer | Images (count) |
| `createdAt` | string | null | Created at: ISO 8601 UTC (Shopify only). |
| `updatedAt` | string | null | Updated at: ISO 8601 UTC (Shopify only). |
| `publishedAt` | string | null | Published at: ISO 8601 UTC (Shopify only). |
| `description` | string | null | Description: Plain text, product rows only, with includeDescription. |
| `variantId` | string | Variant ID: Variant rows only. |
| `variantTitle` | string | null | Variant title: Variant rows only (e.g. 9 / Black). |
| `sku` | string | null | SKU: Variant rows only. |
| `grams` | number | null | Weight (g): Variant rows only. |
| `image` | string | null | Image: Variant rows only: the variant's image, else the product's first image. |
| `storeName` | string | null | Store name: Summary records only (Shopify /meta.json). |
| `productCount` | integer | null | Products in store: Summary records only: published products the store reports (Shopify /meta.json, WooCommerce X-WP-Total). |
| `collections` | array | Collections: Summary records only: the Shopify collections listed. |
| `resultsDelivered` | integer | Rows delivered: Summary records only. |
| `duplicates` | integer | Duplicates: Summary records only: rows already delivered for another entry of this run (not charged). |
| `stopReason` | string | Stop reason: Summary records only: end \| limit \| free_plan \| budget \| deadline \| aborted \| max_pages \| empty_pages \| error |
| `status` | string | Status: ok \| not_found \| error \| skipped_invalid \| skipped_budget \| skipped_free_plan \| skipped_unmetered |
| `charged` | boolean | Charged: Product and variant rows: charged product. Summary records: charged store when at least one row was delivered. |
| `chargedEvent` | string | Charged event: Pay-per-event event charged for this record (present only when charged). |
| `chargedCount` | integer | Events charged: Events actually charged when the charge came back short of the requested count (present only then; a warning explains it). |
| `source` | string | Source: Upstream data source (storefront). |
| `fetchedAt` | string | Fetched at: ISO timestamp. |
| `error` | object | Error: { code, message, provider?, status? } when status is not ok. Actor codes: invalid_input, unsupported_platform, store_password_protected, blocked, collection_not_found. Upstream codes: http_error (see status), timeout, network, dns_not_found, blocked_url, too_large, invalid_json, unexpected_type, invalid_response. Codes set by the runtime: budget_exhausted, free_plan_limit, unmetered, aborted, charge_failed, record_too_large, invalid_record, dataset_push_failed. |
| `warnings` | array | Warnings: Notes about this record, e.g. a free-plan cap, the budget, or a collection that was not found. |

Example record:

```json
{
  "input": "https://www.allbirds.com",
  "recordType": "product",
  "position": 1,
  "platform": "shopify",
  "store": "www.allbirds.com",
  "productId": "7340901859408",
  "handle": "womens-allbirds-flip-flop-dusty-pink",
  "title": "Women's Allbirds Flip Flop - Dusty Pink",
  "url": "https://www.allbirds.com/products/womens-allbirds-flip-flop-dusty-pink",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "tags": [
    "allbirds::edition => limited",
    "shoprunner"
  ],
  "categories": [],
  "price": 25,
  "priceMin": 25,
  "priceMax": 25,
  "compareAtPrice": 50,
  "currency": "USD",
  "onSale": true,
  "discountPct": 50,
  "available": true,
  "variantCount": 7,
  "variants": [
    {
      "variantId": "42146889039952",
      "title": "5",
      "sku": "A12513W050",
      "price": 25,
      "compareAtPrice": 50,
      "available": false,
      "options": [
        "5"
      ],
      "grams": 455
    },
    {
      "variantId": "42146889072720",
      "title": "6",
      "sku": "A12513W060",
      "price": 25,
      "compareAtPrice": 50,
      "available": true,
      "options": [
        "6"
      ],
      "grams": 455
    }
  ],
  "options": [
    {
      "name": "Size",
      "values": [
        "5",
        "6",
        "7",
        "8",
        "9",
        "10",
        "11"
      ]
    }
  ],
  "images": [
    "https://cdn.shopify.com/s/files/1/1104/4168/files/A12513_26Q2_Allbirds-Flip-Flop-Dusty-Pink_PDP_LEFT.png?v=1774646345"
  ],
  "imageCount": 6,
  "createdAt": "2026-03-27T21:03:50.000Z",
  "updatedAt": "2026-10-14T21:56:35.000Z",
  "publishedAt": "2026-09-25T23:58:13.000Z",
  "status": "ok",
  "charged": true,
  "chargedEvent": "product",
  "source": "storefront",
  "fetchedAt": "2026-10-14T12:00:00.000Z"
}
```

## FAQ

**Which stores work?**

Shopify stores that serve their public /products.json (almost all do) and WooCommerce stores with the Store API (WooCommerce 6.0 and later). The platform is detected per store. Password-protected stores, stores that block automated requests and other platforms return not_found or error and are not charged.

**Do I get stock quantities or sales numbers?**

No. Storefronts publish whether a variant is available, not how many units are left, and never sales. available is true when at least one variant can be bought.

**How do I scrape only a collection?**

Give the collection URL (https://store.com/collections/sale) as the store, or list collection handles in collections. Products in several collections are delivered once.


## Related

- [Shopify Store Analyzer – Apps, Theme, Prices](https://www.sfoln.com/actors/shopify-store-analyzer.md) (`sfoln/shopify-store-analyzer`)
- [Google Shopping Scraper](https://www.sfoln.com/actors/google-shopping-search.md) (`sfoln/google-shopping-search`)
- [Amazon Search Results Scraper](https://www.sfoln.com/actors/amazon-search.md) (`sfoln/amazon-search`)

Publisher: SFOLN LLC, contact@sfoln.com. Catalogue: https://www.sfoln.com/catalog.json. Agent guide: https://www.sfoln.com/agents.md.
