# Shopify Stores API Velocity, Pricing & Brand Model (`spool/shopify-stores-api`) Actor

Analyse any Shopify store from its public catalog: launch velocity in products per day, price positioning, discount strategy and brand model.

- **URL**: https://apify.com/spool/shopify-stores-api.md
- **Developed by:** [Spool](https://apify.com/spool) (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 $21.00 / 1,000 stores

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Stores API — Velocity, Pricing & Brand Model

**Give it a Shopify store. Find out how fast it ships, what it charges, how hard
it discounts, and whether it's an own brand or a reseller.**

```
kith.com             5.4 new/day    5.6 published/day   $1,611   active        luxury
gymshark.com         0.0 new/day   15.8 published/day      $18   republishing  budget
finisterre.com       1.9 new/day    3.3 published/day     $137   steady        mid-market
allbirds.com         0.0 new/day    0.1 published/day     $115   dormant       mid-market
```

Gymshark and Allbirds both created zero new products in the last 90 days. One is
re-merchandising its catalog sixteen times a day; the other has genuinely gone
quiet. **Every tool that reads one date tells you those two stores are the
same.**

Every Shopify store publishes its whole catalog at `/products.json` — no key, no
login, permitted by robots.txt. Each product carries the date it went live,
which means **a store's growth rate is readable from a single request.**

***

### Two dates, two different businesses

Every Shopify product carries `created_at` and `published_at`, and almost
everything built on this endpoint picks one and hopes.

`published_at` is rewritten in bulk whenever a store replatforms or rebuilds its
theme. Allbirds spans **7.6 years by `created_at` and 83 days by
`published_at`** — a decade of catalog stamped with one afternoon in June.
Deriving a launch rate from it is wrong in both directions at once:

```
                  from published_at     measured properly     error
allbirds.com           3.54/day              0.00/day         infinite
gymshark.com          12.39/day              0.00/day         infinite
finisterre.com         0.28/day              1.86/day         6.6x too low
rothys.com             0.22/day              0.81/day         3.7x too low
deathwishcoffee.com    0.03/day              0.14/day         4.7x too low
```

Wrong on every store tested — overstating dead ones by an unbounded factor and
understating live ones several-fold.

So both rates are reported. `perDay` counts products genuinely **created** in
the last 90 days; `publishPerDay` counts what the store **published** in the
same window. The gap between them is the finding:

| | new/day | published/day | reading |
|---|---|---|---|
| gymshark.com | 0.0 | 15.8 | `republishing` — re-merchandising, very much alive |
| allbirds.com | 0.0 | 0.1 | `dormant` — actually winding down |
| kith.com | 26.4 | 27.8 | `constant` — genuinely shipping new product |

***

### It tells you when it doesn't know

A 90-day window only works if the read reached back 90 days, and on a fast store
it doesn't. Kith reads **2.8/day at 250 products, 10.9 at 1,000 and 26.4 at
2,500** — still climbing.

Most tools present that as fact. This one flags it. `velocityConfidence` comes
back `low`, and because truncation can only ever hide recent products and never
invent them, `perDay` is then a **lower bound** rather than a wrong number.

It is not a blanket disclaimer — it fires only when it should:

```
organicbasics.com   250 products → 1.76/day  high      full catalog → 1.77/day
finisterre.com      250 products → 1.86/day  high      full catalog → 1.86/day
rothys.com          250 products → 0.81/day  high      full catalog → 0.81/day
kith.com          2,500 products → 26.4/day  low       still rising
```

Organic Basics called correctly to within 0.6% from a default read, while Kith —
the one case that genuinely can't be resolved cheaply — says so. `publishPerDay`
gets the same treatment, with an exact test rather than an estimated one.

***

### Detecting a replatform, and why the obvious test fails

`catalogRepublished` is worth filtering on by itself: a store that just migrated
is a store currently buying apps, themes and agency time.

Getting it right took three attempts. Comparing the created span against the
published span returned `true` for **every store tested** at 250 products —
including Rothy's, Organic Basics and Finisterre, whose complete reads prove
they were never republished. The busiest-publish-day share failed the same way.
Both collapse for one reason: the catalog is paged newest-published-first, so
any truncated read looks compressed in publish time no matter what the store
did.

What survives is the **lag between a product being created and being published**,
because that is a property of each product rather than of the read window:

```
                  lag @250   @500   @2500      verdict
finisterre.com       36d      37d     39d      normal
rothys.com           55d      50d     41d      normal
kith.com            1.8d     1.8d    1.9d      normal
allbirds.com        596d     559d      —       republished
gymshark.com        697d     724d    136d      republished
```

Stores publishing normally measure 2–106 days; stores that rewrote their catalog
measure 136–724. **The verdict is now identical at every read depth for all
eight test stores** — it was wrong for five of them before.

***

### Which apps the store pays for

The catalog tells you what a store sells. The storefront tells you how it
**operates** — and that is the part an app developer or agency buys.

```
organicbasics.com   9 apps   Klaviyo · Judge.me · Yotpo · Narvar · ShopMy · Klarna
deathwishcoffee.com 8 apps   Klaviyo · Postscript · Rebuy · Smile.io · Afterpay
finisterre.com      7 apps   Okendo · Gorgias · Klevu · AB Tasty · Elevar · Klarna
rothys.com          2 apps   Intelligems · Yotpo
```

Over 50 apps are recognised across email/SMS, reviews, support, subscriptions,
CRO, loyalty, search, analytics, post-purchase, payments and affiliate — each
labelled with its category. **Anything third-party that cannot be named is still
reported** in `otherThirdPartyHosts`, so a store running something unusual shows
up rather than looking simpler than it is.

Set `requireApps` and it becomes a lead list: *every store running Klaviyo but
not Recharge* is one run.

The **theme** comes with it, and `themeIsStock` flags the free Shopify themes.
A store still on stock Dawn has not invested in design — which separates a real
operation from a side project more reliably than catalog size does.

***

### Prices you can actually compare

`products.json` carries prices as bare numbers with **no currency anywhere in
it**. A UK store showing `115` and a US store showing `115` are £115 and $115,
and a yen-priced store looks like luxury goods at about a hundred dollars.

So the currency is read from the store profile and every price is reported
twice — in the store's own currency and in USD:

```
finisterre.com      GBP   median £102   →  $137.01   mid-market
organicbasics.com   EUR   median  €41   →   $46.60   mid-market
allbirds.com        USD   median $115   →     $115   mid-market
```

Price positioning is judged on the USD figure, never the raw number — reading
Finisterre's `102` as dollars drops it a whole bracket, and a yen-priced store
would read as budget goods. When a rate is unavailable the USD fields are left **null** rather
than filled with a guess.

***

### Quick start

```json
{ "storeUrls": ["allbirds.com", "kith.com", "rothys.com"] }
```

Domains, full URLs and bare handles all work. `www.` is stripped, and
`www.x.com` and `x.com` are merged so you're never billed twice for one store.

***

### What you get back

```json
{
  "store": "finisterre.com",
  "storeName": "Finisterre",
  "country": "GB",
  "city": "St Agnes",
  "currency": "GBP",
  "shipsToCountryCount": 39,

  "productsSampled": 500,
  "catalogComplete": false,

  "perDay": 1.86,
  "velocityConfidence": "high",
  "publishPerDay": 3.33,
  "publishConfidence": "high",
  "velocitySample": 167,
  "velocityWindowDays": 90,
  "newestAt": "2026-09-14T15:41:10.000Z",

  "catalogRepublished": false,
  "medianPublishLagDays": 37.05,
  "republishShare": 0.08,

  "priceMin": 6, "priceMedian": 102.5, "priceMax": 295,
  "priceMedianUsd": 137.01,
  "onSaleShare": 0.12, "averageMarkdown": 0.35,
  "outOfStockShare": 0.1,

  "vendorCount": 1,
  "topVendors": [{ "name": "Finisterre", "count": 500 }],
  "productTypes": [{ "name": "Men's Jackets", "count": 41 }],

  "theme": "LinedUp / BFVA",
  "themeIsStock": false,
  "appCount": 7,
  "apps": ["AB Tasty", "Back in Stock (Amp)", "Elevar", "Klarna", "Klevu", "Okendo"],
  "appsByCategory": { "reviews": ["Okendo"], "support": ["Gorgias"], "cro": ["AB Tasty"] },
  "socialChannels": ["instagram", "facebook", "youtube"],

  "signals": {
    "launchCadence": "steady",
    "pricePositioning": "premium",
    "discountStrategy": "occasional",
    "brandModel": "own-brand",
    "catalogScale": "mid",
    "inventoryPressure": "normal"
  }
}
```

`signals` carries the reading; every threshold is published in
`signals.thresholds`, so you can disagree with an interpretation without losing
the numbers behind it.

Six dataset views ship with it: **Overview**, **Apps & tech**, **Growth**,
**Pricing**, **Catalog** and **Unavailable**.

***

### Recipes — copy, paste, run

**Which of my competitors is actually shipping?**

```json
{ "storeUrls": ["rival-one.com", "rival-two.com", "rival-three.com"] }
```

**Find the resellers in a list — who stocks other people's brands**

```json
{
  "storeUrls": ["store-a.com", "store-b.com"],
  "requireSignals": ["brandModel:multi-brand-reseller"]
}
```

**Stores in trouble — heavy discounting and empty shelves**

```json
{
  "storeUrls": ["..."],
  "requireSignals": ["discountStrategy:permanent-sale", "inventoryPressure:mostly-unavailable"]
}
```

**Stores that just replatformed — buying apps, themes and agency time right now**

```json
{
  "storeUrls": ["..."],
  "requireSignals": ["launchCadence:republishing"]
}
```

**Exact catalog and full price range on a big store**

```json
{ "storeUrls": ["gymshark.com"], "maxProductsPerStore": 5000 }
```

***

### Every option

| Option | Default | What it does |
|---|---|---|
| **`storeUrls`** | — | **Required.** Domains, URLs or handles, mixed freely |
| `maxProductsPerStore` | `500` | How deep to read. The default settles velocity on most stores; when it cannot, `velocityConfidence` says so. Raise it for an exact catalog count and `lifetimePerDay` |
| **`requireApps`** | *(none)* | **Keep only stores running** `Klaviyo`, `Recharge`, `Gorgias`… The lead-list filter |
| `requireAppsMode` | `any` | `all` finds a specific stack, like Klaviyo **and** Recharge |
| `detectApps` | `true` | Read the storefront for apps, theme and socials. One extra request per store |
| **`requireSignals`** | *(none)* | **Keep only stores matching** `launchCadence:constant`, `brandModel:multi-brand-reseller`, `pricePositioning:luxury`… |
| `minProducts` | `0` | Skip stores smaller than this — filters out test and abandoned shops |
| `includeProducts` | `false` | Add every product with title, URL, vendor, tags, date and price |
| `maxConcurrency` | `5` | Stores analysed in parallel |

***

### What it covers, honestly

**About 5 stores in 6 work.** Tested against 18 real stores: **15 served the
catalog**, 3 did not. Those three return `not_a_shopify_store` — either they've
disabled the endpoint or they're not on Shopify. A miss is always reported, never
silent.

Stripping `www.` matters more than it sounds: three of those stores failed with
the `www.` prefix and succeeded without it, so it's normalised for you.

**`catalogSize` is exact only when `catalogComplete` is true.** On a catalog
larger than your read limit, the count is null rather than a number that would
be wrong. `productsSampled` always tells you what was actually read.

**Price and discount figures come from the sample**, which is the newest end of
the catalog. For a complete catalog they're exact; otherwise they describe
current stock rather than everything ever sold.

**Currency and shipping reach are what the store serves *you*.** Shopify
switches storefront by geography, so an Italian brand answered in USD when
tested from outside the EU, and reported shipping to 2 countries rather than
its full list. The figures are reported exactly as the store gave them rather
than normalised into something that looks tidier but is invented.

**Availability depends on the store.** Some publish it, some don't —
`stockDataAvailable` says which, rather than reporting a misleading zero.

**App detection reads what the storefront loads.** A large retailer that
self-hosts its scripts shows fewer apps than it runs — Gymshark detects only 1 —
so a low count on a big brand means "not visible", not "not installed".

**No proxy, no key, no blocking.** Two or three plain HTTPS requests per store.

***

### When a store can't be read

| `error` | Meaning |
|---|---|
| `not_a_shopify_store` | No public catalog at `/products.json` — disabled, or not Shopify |
| `catalog_blocked` | The store returned 403 or 429 |
| `catalog_empty` | Catalog exists but has no products |
| `invalid_input` | Couldn't read a domain from that value |

A breakdown is saved to the key-value store as `RUN_SUMMARY`.

***

### FAQ

**How do I find a Shopify store's product catalog?**
Pass the domain. The Actor reads the public `/products.json` endpoint that every
Shopify store serves, and returns the catalog plus derived metrics.

**How is this different from a product scraper?**
Product scrapers hand you rows. This measures the store: how fast it ships, where
it prices, how it discounts, whether it resells. You can get to a shortlist
without reading a single product.

**Can it tell me how much a store earns?**
No, and nothing honestly can from public data. Tools quoting revenue are
estimating. This reports what the store itself publishes — catalog, prices,
dates, availability — and derives only what those support.

**Does it work on any store?**
Roughly five in six. Some owners disable the public catalog, and those are
reported rather than guessed at.

**Is this legal?**
`/products.json` is a public endpoint Shopify serves by design, it requires no
authentication, and robots.txt permits it. Everything returned is catalog data —
products, prices, dates. No customer data, no personal information.

***

### How to use it

1. Open the Actor and paste your store domains into **Shopify stores** — one per line, `www.` or not.
2. Optionally set **requireApps** (e.g. `Klaviyo`) or **requireSignals** (e.g. `brandModel:multi-brand-reseller`) to turn the scan into a shortlist.
3. Click **Start**. Ten stores take about ten seconds.
4. Open the **Growth**, **Pricing** or **Apps & tech** dataset view, or export the whole thing as CSV, Excel or JSON.
5. To repeat it weekly, use **Schedule**; to pull it into your own system, call the run through the API or an integration (Google Sheets, Make, Zapier, webhooks).

***

### How much does it cost?

Pay per event — you are charged **per store analysed**, and nothing for stores that could not be read.

| Plan | per store |
|---|---|
| Free | $0.03 |
| Bronze | $0.027 |
| Silver | $0.024 |
| Gold | $0.021 |

A store that was read but filtered out by your `requireSignals` / `minProducts` settings costs $0.002. Actor start is $0.002.

**Worked examples**

- Analyse **10 competitors** → 10 × $0.03 = **$0.30**
- Scan **100 Shopify stores** for ones running Klaviyo → about **$3.00** (Gold plan: $2.10)
- The **free plan's $5 credit** covers roughly **160 stores**

No proxy is used, so there is no proxy bill on top.

***

### More Shopify tools from spool

- **[Shopify Product Scraper — GTIN & Collections](https://apify.com/spool/shopify-product-feed)** — every product and variant with GTIN barcodes, subscription plans, wholesale breaks and best-seller membership
- **[Shopify Scraper — Stores & Products](https://apify.com/spool/shopify-store-products-scraper)** — both of the above in one run — the store summary plus the full catalog

All three read the same public endpoints, respect robots.txt, and report a miss rather than guessing.

***

### Support

Open an issue on the **Issues** tab and you'll get a reply, usually the same day.
Requests for extra metrics are welcome — tell me what you need measured.

# Actor input Schema

## `storeUrls` (type: `array`):

Domains or full store URLs — both work, and `www.` is stripped for you.

• `allbirds.com`
• `https://www.gymshark.com/collections/mens`
• `deathwishcoffee` (a bare handle becomes `deathwishcoffee.myshopify.com`)

Duplicates are merged, so `www.x.com` and `x.com` are analysed once.

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

Shopify serves 250 products per request, newest first.

**Most stores need nothing more than the default.** Launch velocity is counted over the last 90 days, and on a normal store 250 products reach back further than that — Organic Basics reads 1.76/day from 250 products against 1.77/day from its full 1,816.

On a very fast store the read limit cuts into the window. When that happens `velocityConfidence` comes back `low` and `perDay` is a lower bound rather than a wrong number — raise this to resolve it.

Also raise it for an exact catalog count, a full price range, and `lifetimePerDay`, all of which need the whole catalog.

## `requireApps` (type: `array`):

**This is the lead list.** Name the apps a store must have — one per line, spelled as they appear in the output.

`Klaviyo` · `Recharge` · `Gorgias` · `Yotpo` · `Okendo` · `Rebuy` · `Attentive` · `Judge.me` · `Loop Returns` · `Smile.io` · `Searchanise` · `Triple Whale` · `Klarna` · `Afterpay`

If you sell a Shopify app, this finds the stores already paying for something adjacent to yours. Detected from the storefront, not guessed.

## `requireAppsMode` (type: `string`):

`any` casts a wide net. `all` finds a specific stack — for example Klaviyo **and** Recharge, which is a subscription brand running serious email.

## `requireSignals` (type: `array`):

Written as `key:value`, one per line. This is what turns a scan into a shortlist:

• `launchCadence:constant` — shipping 20+ products a day
• `launchCadence:republishing` — just replatformed and re-merchandising its whole catalog, which is when a store is buying apps, themes and agencies
• `brandModel:multi-brand-reseller` — stocks other people's brands
• `pricePositioning:luxury` — median price $500+
• `discountStrategy:permanent-sale` — always on sale
• `inventoryPressure:mostly-unavailable` — 60%+ out of stock
• `catalogScale:enterprise` — 5,000+ products

Leave empty to keep every store.

## `minProducts` (type: `integer`):

Skip stores smaller than this. Useful for filtering out dropshipping test stores and abandoned shops when scanning a long list.

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

Adds every product read to the row — title, URL, vendor, type, tags, publish date, variant count and price. Off by default so the output stays one clean line per store.

## `detectApps` (type: `boolean`):

Adds one request per store and returns installed apps, theme, and social channels. On by default — it is the most valuable part of the output. Turn off for a pure catalog run.

## `maxConcurrency` (type: `integer`):

Stores analysed in parallel. Each store is one or two plain HTTPS requests with no proxy involved, so the only reason to keep this modest is politeness.

## Actor input object example

```json
{
  "storeUrls": [
    "allbirds.com",
    "kith.com",
    "rothys.com"
  ],
  "maxProductsPerStore": 500,
  "requireApps": [],
  "requireAppsMode": "any",
  "requireSignals": [],
  "minProducts": 0,
  "includeProducts": false,
  "detectApps": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Every store analysed, with all metrics and derived signals.

## `growth` (type: `string`):

Launch velocity and catalog age — who is shipping and who has stopped.

## `pricing` (type: `string`):

Price bands and discount behaviour across the stores.

## `runSummary` (type: `string`):

Stores analysed, filtered and unreachable, with failures by type.

# 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 = {
    "storeUrls": [
        "allbirds.com",
        "kith.com",
        "rothys.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("spool/shopify-stores-api").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 = { "storeUrls": [
        "allbirds.com",
        "kith.com",
        "rothys.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("spool/shopify-stores-api").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 '{
  "storeUrls": [
    "allbirds.com",
    "kith.com",
    "rothys.com"
  ]
}' |
apify call spool/shopify-stores-api --silent --output-dataset

```

## MCP server setup

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

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/6TxSNzRV96HIuhm91/builds/uNGO7SkPSjgPlKukQ/openapi.json
