# Shopify Intelligence Tracker (`mollyvir/shopify-intelligence-tracker`) Actor

Pull structured product data from any Shopify storefront - no API key needed. One row per variant with prices, SKUs, stock status, images, tags and weights, from the public product feed. Built for competitor research, price monitoring and catalog audits. Billed per product, not per variant.

- **URL**: https://apify.com/mollyvir/shopify-intelligence-tracker.md
- **Developed by:** [Molly Vir](https://apify.com/mollyvir) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 63.2% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 product scrapeds

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?

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 Intelligence Tracker

**Pull clean, structured product data out of any Shopify storefront — no API key, no scraping code.**

Give it a storefront URL. It returns one row per product variant with prices, stock status, images, SKUs, tags, weights and options, taken straight from the store's public product feed.

Built for price monitoring, competitor research, catalog audits and catalog-import workflows.

### Example input

```json
{
  "urls": ["https://www.everlane.com"],
  "max_products": 25
}
```

### Example output

One row per variant. Real record from a live Everlane run:

```json
{
  "storeDomain": "https://www.everlane.com",
  "productName": "Pull-On Maxi Skirt in Cotton Silk | White",
  "variantName": "XXS",
  "option1": "XXS",
  "price": "138.00",
  "sku": "F-BTM-VLE-MAXI-SKIRT-WHT-XXS",
  "inStock": true,
  "productUrl": "https://www.everlane.com/products/womens-pull-on-maxi-skirt-in-cotton-silk-white",
  "productId": 7859023741014,
  "variantId": 44641683898454,
  "handle": "womens-pull-on-maxi-skirt-in-cotton-silk-white",
  "vendor": "Everlane",
  "productType": "Bottoms",
  "tags": ["category: bottoms", "fabric: silk", "Style SKU: F-BTM-VLE-MAXI-SKIRT"],
  "imageUrl": "https://cdn.shopify.com/s/files/1/0623/7916/3734/files/2026-09-27_F-BTM-VLE-MAXI-SKIRT-WHT_flat.jpg",
  "weight": 1901,
  "weightUnit": "g",
  "requiresShipping": true,
  "taxable": true
}
```

### Pricing

Pay per event. You are charged for what you get, and the price does not depend on how long the run takes or how much bandwidth it uses.

| Event | Price | When it is charged |
| --- | --- | --- |
| Product scraped | **$0.0012** | Once per product found, including all of its variants |
| Storefront | **$0.002** | Once per storefront URL requested |
| Actor Start | $0.00005 | Once per run, when the run begins |

**Example:** one storefront, 100 products, any number of variants per product:

```
Actor Start      1  × $0.00005  =  $0.00005
Storefront       1  × $0.002    =  $0.00200
Product scraped  100 × $0.0012  =  $0.12000
                                   --------
Total                              $0.12205
```

A product with twelve variants still costs $0.0012, so you never pay per row. Duplicate products and products with no variant data are not charged. A storefront that fails is still charged, because it still consumed network resources — but a URL that is rejected as invalid before any request is made is not charged.

There is no minimum charge and no separate platform-usage fee. If a run finds nothing, you pay nothing.

### Features

- **No API key.** Uses the public product feed every Shopify store already exposes.
- **Every variant, not just products.** One row per variant with its own price, SKU and stock status.
- **Residential proxy by default.** Datacenter IPs are blocked by most Shopify stores, so the default configuration routes through residential proxies that storefronts actually accept.
- **Works with many storefronts per run.** Pass as many URLs as you like in a single run.
- **Automatic pagination.** Follows the feed page by page until your product cap or the end of the catalog.
- **Deduplicated.** Products appearing on multiple pages and repeated records are removed.
- **Retry-aware.** Rate limits and transient server errors are retried with exponential backoff.
- **Honest failures.** If every storefront fails, the run exits non-zero instead of reporting an empty success, so scheduled runs and monitors can tell the difference.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `urls` | array of strings | — | Shopify storefront URLs to scan, for example `https://example.com`. Maximum 50 per run; split larger batches across runs. |
| `max_products` | integer | `25` | Maximum products per storefront. Minimum `1`, maximum `100000`. |
| `proxyConfiguration` | object | `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` | Proxy routing. See [Proxies](#proxies); this default matters. |

### Output

One item per product variant:

| Field | Type | Description |
| --- | --- | --- |
| `storeDomain` | string | Base URL of the storefront. |
| `productName` | string | null | Product name. |
| `variantName` | string | null | Variant name. |
| `option1` / `option2` / `option3` | string | null | Variant options, such as size or color. |
| `price` | string | null | Current variant price as reported by the storefront. |
| `compareAtPrice` | string | null | Comparison price when supplied by Shopify. |
| `sku` | string | null | Merchant SKU. |
| `inStock` | boolean | null | Whether Shopify reports the variant as currently available. Reflects public availability only, not an inventory quantity. |
| `productUrl` | string | null | Direct link to the product page. |
| `productId` | integer | null | Shopify product identifier. |
| `variantId` | integer | null | Shopify variant identifier. |
| `handle` | string | null | Product URL handle. |
| `vendor` | string | null | Vendor or brand. |
| `productType` | string | null | Product category or type. |
| `tags` | array\<string> | null | Product tags, normalized into a list. |
| `createdAt` / `updatedAt` / `publishedAt` | string | null | Product timestamps. |
| `imageUrl` | string | null | Variant image from `featured_image`, falling back to the product's first image. |
| `barcode` | string | null | Variant barcode or UPC. Shopify's public `products.json` feed does not include barcodes, so this is normally `null`. |
| `weight` | number | null | Variant weight in **grams**, as reported by Shopify. |
| `weightUnit` | string | null | Always `"g"` when a weight is present, otherwise `null`. |
| `requiresShipping` | boolean | null | Whether the variant requires shipping. |
| `taxable` | boolean | null | Whether the variant is taxable. |

Prices are reported exactly as Shopify returns them and are not labeled with any currency.

### Proxies

**This Actor does not work without a proxy.** Apify Actors send requests from datacenter IP ranges, and most Shopify storefronts block those outright with HTTP 429. A run without a proxy returns zero items, and fails with a non-zero exit code rather than reporting a misleading success.

The default `proxyConfiguration` routes requests through Apify's **residential** proxy group, which storefronts accept. Verified against live stores.

The default Apify proxy group when you only set `useApifyProxy: true` is **datacenter**, which Shopify blocks just as hard as a direct request. `RESIDENTIAL` is specified explicitly for that reason.

This default is applied by the Actor itself, not only by the run form. Runs started through the API, the CLI, a schedule or the Apify MCP server do not get input-schema defaults applied, so without a code-level default those callers would silently fall back to `DATACENTER`. Passing `proxyConfiguration` explicitly always overrides it.

| Setting | Behaviour | Typical cost |
| --- | --- | --- |
| `apifyProxyGroups: ["RESIDENTIAL"]` (default) | Works against most stores | ~$8 / GB, absorbed by the Actor price |
| `apifyProxyGroups: ["DATACENTER"]` | Cheaper, blocked by most stores | from $0.6 / IP |
| `useApifyProxy: false` | Direct request, no proxy | Free, but returns nothing |

Each storefront is fetched through a single proxy session, so all pages and retries for one store come from the same IP. Rotating mid-crawl makes the request pattern look erratic and defeats backoff.

The one exception is a broken proxy session. When the proxy itself returns an upstream error, retrying on the same session cannot succeed, so the Actor transparently retries that storefront on a fresh session. A store answering 429 or 404 keeps its original IP and is never rotated, since that would defeat backoff. Because every network call happens before any data is pushed or charged, a rotated retry cannot bill the same storefront twice.

### How it works

- Uses the storefront's public `products.json` feed.
- Routes each storefront through its own proxy session (see [Proxies](#proxies)).
- Paginates with page-based requests (up to 250 products per page) until `max_products` is reached or the feed is exhausted.
- Deduplicates products that appear on multiple pages.
- Retries rate-limited (HTTP 429) and server errors (HTTP 5xx) with exponential backoff, three attempts per request.
- Output records are deduplicated by `(productId, variantId, productUrl)`.
- Exits non-zero if every storefront failed, so scheduled runs and monitoring can distinguish failure from an empty result.

### Limitations

Worth knowing before you rely on this:

- **50 storefronts per run.** Larger batches are truncated to the first 50 and the log says how many were dropped. Each URL consumes proxy bandwidth, so the limit keeps a single run's cost bounded.

- **Only Shopify stores.** The Actor reads the public `products.json` feed that Shopify generates. Other platforms have no equivalent, and will fail with a clear error.

- **Public data only.** This reads what the storefront publishes to its public feed. Discount codes, customer-specific pricing, inventory counts and sold-out items hidden from the feed are not available. Prices reflect the store's public list price, not what a customer would actually pay at checkout.

- **Some stores rate-limit aggressively.** A small number of high-traffic stores return HTTP 429 even through residential proxies. Retry the run, or lower `max_products`.

- **Some stores block automated clients outright.** The Actor identifies itself honestly as `ShopifyIntelligenceTracker/1.0` and links to its Apify page, rather than claiming to be a browser. When a store refuses anyway, the Actor retries from a second and third residential IP before giving up, so an IP-level block usually clears. A store that blocks bot traffic on every IP will still fail.

- **No barcodes.** Shopify does not publish barcodes in `products.json`, so `barcode` is normally `null`.

- **No currency.** Prices are raw strings from the store, without a currency code.

- **`inStock` is a flag, not a count.** It reflects public availability, not inventory quantity.

### Troubleshooting

**Every storefront failed, run exited non-zero.**
Most likely the proxy is not working. Check that `proxyConfiguration` still includes `apifyProxyGroups: ["RESIDENTIAL"]`. The Actor logs the exact failure per storefront, including HTTP status or network error type.

**One storefront returned nothing.**
Either the URL is not a Shopify store, or that store is blocking the request. The log names the storefront and the reason, including every IP that was tried.

**A storefront failed with HTTP 403.**
The Actor retries from up to three residential IPs before failing that storefront, so an IP-level block usually clears on its own. If it still fails, the store is blocking automated traffic regardless of source IP. Raising an issue is worth it if the store is widely used.

**A product is missing a field such as `imageUrl` or `sku`.**
That store does not publish the field. Coverage is very high across stores, but it is never 100% because merchants configure their own data.

**`barcode` is always `null`.**
Expected. See [Limitations](#limitations).

### Development

```
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -r requirements-dev.txt
pytest
```

Run locally:

```
apify run
```

### Input security

Storefront URLs are validated before fetching. Private IPs, loopback, link-local, reserved IPs, and localhost targets are rejected to avoid SSRF.

### Deploy

`apify push`

# Actor input Schema

## `urls` (type: `array`):

Enter one or more Shopify storefront URLs to scan. Up to 50 per run; split larger batches across runs.

## `max_products` (type: `integer`):

Maximum number of products to retrieve from each storefront. Products are fetched automatically via pagination, up to this cap.

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

Most Shopify storefronts block datacenter IP ranges, which is what Apify Actors use by default. Without a proxy this Actor returns HTTP 429 and collects nothing. The default below routes requests through Apify's residential proxy group, which storefronts accept. Residential proxy bandwidth is billed per GB (about $8/GB), so a 250-product page costs roughly $0.005. Set apifyProxyGroups to \["DATACENTER"] to use the cheaper datacenter IPs, or useApifyProxy to false to send requests directly - expect most stores to block that.

## Actor input object example

```json
{
  "urls": [
    "https://www.everlane.com"
  ],
  "max_products": 25,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

Dataset containing the products and variants discovered during this run.

# 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 = {
    "urls": [
        "https://www.everlane.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mollyvir/shopify-intelligence-tracker").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 = { "urls": ["https://www.everlane.com"] }

# Run the Actor and wait for it to finish
run = client.actor("mollyvir/shopify-intelligence-tracker").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 '{
  "urls": [
    "https://www.everlane.com"
  ]
}' |
apify call mollyvir/shopify-intelligence-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mollyvir/shopify-intelligence-tracker"
        }
    }
}
```

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/lAjR7gIehqo1gdnV4/builds/gjlcqBoba0l3dCrev/openapi.json
