# Shopify Store Scraper · Qualified Leads from Any Domain List (`thequietstack/shopify-store-scraper`) Actor

Paste store domains, get one qualified row per Shopify store: exact product count, price band, newest product date, theme, location, currency, public e-mails, phones, socials and lead signals. Non-Shopify, password and duplicate domains are never charged.

- **URL**: https://apify.com/thequietstack/shopify-store-scraper.md
- **Developed by:** [TheQuietStack](https://apify.com/thequietstack) (community)
- **Categories:** E-commerce, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 scraped stores

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 Store Scraper · Qualified Leads from Any Domain List

Paste a list of store domains, from a lead export, a Google Maps list, a competitor list or a CSV. You get **one qualified row per Shopify store**: whether it really is a public Shopify storefront, how many products it sells (counted, not estimated), price band, when it last published a product, theme, location and currency, the **contact details the store itself shows publicly**, and **lead signals** such as *no new product in 90 days* or *outdated copyright year*.

Domains that turn out **not** to be Shopify, **password-protected**, **headless**, **behind a bot wall** or **duplicates** of a store you already listed are reported in the run summary and **never charged**.

No login. No API key. Nothing is guessed or enriched from third-party databases.

### Who it is for

- **Agencies and freelancers** who sell Shopify design, SEO, apps, photography or support: find stores that have gone quiet, show no phone number, or still carry a 2021 footer.
- **Sales teams** qualifying a list before outreach: drop dead stores and stores outside your size range *before* you pay.
- **Market research**: price bands, catalog size and publishing pace of a set of competitors, side by side.

### Why this scraper

- **Counted, not estimated.** Shopify's own store metadata reports a product count, but we measured on 24 Sep 2026 that it **changes with the visitor's market**: for one store it said **212** products with a US English browser and **608** without, while the store's product feed listed **608** both ways. This Actor counts the products itself. `productCount` is filled only when the whole catalog was read; otherwise you get `productCountAtLeast` and Shopify's figure in `productCountShopifyMeta`, never a number dressed up as exact.
- **Freshness from the right date.** Shopify bumps `updated_at` on every product whenever inventory syncs (all 250 products on a page showed the same day in our tests), so "last updated" says nothing about the merchant. We use the **publish date**. Shopify lists newest products first, so `daysSinceNewestProduct` is exact even for stores with 25,000 products.
- **You pay for stores, not for domains.** Not Shopify, password page, headless front end, bot wall, dead domain, or the same store under a second domain (`www.`, `.myshopify.com` alias, same shop ID): all listed in the `SUMMARY`, none charged. No silent zero-result runs: every input domain is accounted for.
- **Lead signals you can filter on.** Filtered-out stores are not charged.

### Input example

```json
{
    "domains": ["allbirds.com", "chalk-line.com", "horizonmotorsport.com", "not-a-shop.com"],
    "countries": ["US"],
    "maxProductCount": 200,
    "requiredLeadSignals": ["no-new-product-90d", "no-phone-shown"],
    "maxStores": 500
}
```

Set `"includeProducts": true` to also get one row per product (title, URL, type, vendor, tags, price range, compare-at price, availability, image, publish date), newest first.

### Output example (store row, measured 24 Sep 2026)

```json
{
    "name": "Horizon Motorsport",
    "url": "https://horizonmotorsport.com",
    "domain": "horizonmotorsport.com",
    "myshopifyDomain": "horizon-motorsport.myshopify.com",
    "city": "Irvine",
    "province": "California",
    "country": "US",
    "currency": "USD",
    "productCount": 565,
    "productCountAtLeast": null,
    "productCountShopifyMeta": 565,
    "catalogFullyScanned": true,
    "priceMin": 4,
    "priceMedian": 380,
    "priceMax": 6599,
    "priceStatsBasis": "all 565 products",
    "newestProductPublishedAt": "2026-09-15T21:50:35.000Z",
    "daysSinceNewestProduct": 8,
    "productsPublishedLast30Days": 4,
    "productsPublishedLast90Days": 54,
    "topProductTypes": [{ "name": "Front Lip", "count": 51 }, { "name": "Exterior Accessories", "count": 45 }],
    "themeSchemaName": "Streamline",
    "copyrightYear": 2026,
    "emails": ["sales@horizonmotorsport.com"],
    "emailSources": { "sales@horizonmotorsport.com": "contact page" },
    "phones": [],
    "legalName": "Horizon Motorsport",
    "publishedAddress": "Horizon Motorsport Inc, 20 Pebblewood, Irvine CA 92604, United States",
    "instagram": "https://www.instagram.com/horizon.motorsport",
    "hasContactPage": true,
    "leadSignals": ["no-phone-shown"],
    "leadSignalCount": 1
}
```

| Field | Meaning |
|---|---|
| `productCount` | Products we counted in the public product feed. Only set when the whole catalog was read (`catalogFullyScanned`). |
| `productCountAtLeast` | Lower bound when the catalog is bigger than the pages you allowed (`productPagesPerStore` × 250). |
| `productCountShopifyMeta` | Shopify's own figure. Depends on the visitor's market and can differ from the feed. |
| `priceMin` / `priceMedian` / `priceMax` | Lowest variant price per product, across the products read. `priceStatsBasis` says which products. |
| `daysSinceNewestProduct` | Days since the most recently published product. Always exact. |
| `productsPublishedLast30Days` / `…90Days` | Exact, or `null` with `…AtLeast` when the window reaches past the pages read. |
| `onSaleShare` / `soldOutShare` | Share of products with a compare-at price / with no available variant. |
| `themeName` / `themeSchemaName` / `themeSchemaVersion` / `themeStoreId` | From the storefront's public theme object. |
| `emails` / `phones` | Only what the store shows: homepage, its contact page and Shopify's contact-information page. `emailSources` says where each e-mail was found. |
| `legalName` / `publishedAddress` | From Shopify's contact-information page, if the merchant filled it in. |
| `leadSignals` | See below. |
| `homepageRedirectedTo` | Set when the homepage sends visitors to another host (headless front end). Homepage-based signals are then left out instead of guessed. |

#### Lead signals

`no-new-product-90d` · `no-new-product-365d` · `small-catalog-under-20` · `no-products` · `mostly-sold-out` (half or more of the products have no available variant) · `no-email-shown` · `no-phone-shown` · `no-contact-page-found` (no contact link on the homepage and no `/pages/contact`) · `no-social-links` · `outdated-copyright-year` (footer year older than last year) · `no-custom-domain`.

### Run summary

`SUMMARY` in the key-value store lists, per input domain, why it produced no row: `notShopify`, `passwordProtected`, `headless`, `blockedByBotProtection`, `unreachable`, `duplicateStores`, `failed`, invalid inputs, how many stores each filter dropped, and HTTP status counts.

### Honest limits

- **Public storefront data only.** No login, no password pages, no checkout. Password-protected stores are skipped, not opened.
- **No bot-wall bypass.** Some large brands run their storefront behind bot protection or a headless front end; they are listed as `blockedByBotProtection` or `headless` in the summary. In our 24-domain test on 24 Sep 2026: 16 store rows, 1 bot wall (HTTP 429 on every request), 1 password page, 4 not Shopify, 1 dead domain, 1 duplicate alias.
- **No e-mail guessing.** If a store only offers a contact form, `emails` is empty and the row carries `no-email-shown`. That is often exactly the lead you want.
- **Discovery is up to you.** This Actor qualifies domains you already have; it does not search for stores.
- The product catalog shown can depend on the visitor's market. `storefrontLocale` records which locale the store served.

### Pricing (pay per event)

- Actor start: small flat fee per run
- Per scraped store: only Shopify stores that pass your filters and are written to the dataset
- Per scraped product: only when product rows are switched on

# Actor input Schema

## `domains` (type: `array`):

One per line: allbirds.com, https://www.example.com/collections/x or shop.myshopify.com. Duplicates (www / no-www / myshopify alias of the same store) are detected and charged once.

## `productPagesPerStore` (type: `integer`):

Price band, freshness and the product count come from these products. Shopify lists newest products first, so the newest-product date is always exact. productCount is exact (counted by us) when the whole catalog fits into these pages — otherwise productCountAtLeast is set and productCount stays empty. Shopify's own figure is in productCountShopifyMeta; it depends on the market and can differ.

## `checkContactPages` (type: `boolean`):

Reads /pages/contact and Shopify's /policies/contact-information in addition to the homepage. Only contact details the store shows publicly are returned — nothing is guessed or enriched.

## `countries` (type: `array`):

Two-letter codes from the store's own address (US, GB, CA, AU, DE ...). Empty = all. Filtered stores are not charged.

## `minProductCount` (type: `integer`):

Counted products (lower bound if the catalog was not read to the end).

## `maxProductCount` (type: `integer`):

Use e.g. 50 to find small stores.

## `newProductWithinDays` (type: `integer`):

Keeps stores that published a product recently.

## `noNewProductForDays` (type: `integer`):

Keeps stores whose newest product is at least this old — typical re-design / re-activation leads.

## `onlyWithEmail` (type: `boolean`):

Stores without a public e-mail are dropped and not charged.

## `onlyWithPhone` (type: `boolean`):

Stores without a public phone number are dropped and not charged.

## `requiredLeadSignals` (type: `array`):

Any of: no-new-product-90d, no-new-product-365d, small-catalog-under-20, no-products, mostly-sold-out, no-email-shown, no-phone-shown, no-contact-page-found, no-social-links, outdated-copyright-year, no-custom-domain.

## `includeProducts` (type: `boolean`):

Adds one row per product (title, URL, type, vendor, tags, price, compare-at price, availability, image, published date), newest first. Charged per product.

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

Only used with product rows. Limited by the product pages read per store.

## `maxStores` (type: `integer`):

The run stops cleanly at this number. You are never charged for more.

## `maxProducts` (type: `integer`):

Only used with product rows. You are never charged for more.

## `concurrency` (type: `integer`):

Requests to one store are always spaced out; this only sets how many different stores run at once.

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

Default: Apify datacenter proxy, fresh IP per store.

## Actor input object example

```json
{
  "domains": [
    "allbirds.com",
    "chalk-line.com"
  ],
  "productPagesPerStore": 4,
  "checkContactPages": true,
  "countries": [
    "US"
  ],
  "onlyWithEmail": false,
  "onlyWithPhone": false,
  "requiredLeadSignals": [
    "no-new-product-90d",
    "outdated-copyright-year"
  ],
  "includeProducts": false,
  "maxProductsPerStore": 100,
  "maxStores": 500,
  "maxProducts": 5000,
  "concurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `stores` (type: `string`):

Qualified stores: exact product count, price band, freshness, theme, location, public contacts, lead signals.

## `summary` (type: `string`):

Which domains were not Shopify, password-protected, headless, blocked, unreachable or duplicates — none of them charged.

# 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 = {
    "domains": [
        "allbirds.com",
        "chalk-line.com",
        "horizonmotorsport.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("thequietstack/shopify-store-scraper").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 = {
    "domains": [
        "allbirds.com",
        "chalk-line.com",
        "horizonmotorsport.com",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("thequietstack/shopify-store-scraper").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 '{
  "domains": [
    "allbirds.com",
    "chalk-line.com",
    "horizonmotorsport.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call thequietstack/shopify-store-scraper --silent --output-dataset

```

## MCP server setup

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

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/efEgzjReAOj5DAqgp/builds/ALzYNqCt7Onu4qN5A/openapi.json
