# Shopify Products API — Catalogue, Prices & Stock (`insight.solutions/shopify-products-api`) Actor

Scrape any Shopify store's catalogue as data. One row per product or per variant: title, handle, SKU, price, compare-at price, stock, options, images, tags, vendor and collections. Give it a domain — no API key, no app install, no store password.

- **URL**: https://apify.com/insight.solutions/shopify-products-api.md
- **Developed by:** [Insight Solutions](https://apify.com/insight.solutions) (community)
- **Categories:** E-commerce, Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 product returneds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Shopify Products API — Catalogue, Prices & Stock

**Get any Shopify store's catalogue as data.** Give this Actor a list of store domains — or a link to one collection — and get back one row per product: title, handle, SKU, price, compare-at price, what is in stock, every variant, every image, the tags, the vendor and the dates. Ask for one row per variant and each size, colour and SKU arrives on its own line.

No API key. No app to install in anyone's store. No store password, no partner account, no Admin API token. **$0.50 per 1,000 products, proxy included**, stores that are not on Shopify are free, and a run that returns nothing costs nothing at all.

### Try it in 30 seconds

```json
{
  "stores": [
    "https://www.allbirds.com",
    "colourpop.com"
  ],
  "maxProductsPerStore": 100
}
```

One full URL, one bare domain — both work, and so does a `myshopify.com` domain, a product page link, or a collection link like `https://www.allbirds.com/collections/mens`.

### What comes back

```jsonc
{
  "ok": true,
  "rowType": "product",              // "product" | "variant" | "collection" | "diagnostic"

  "storeDomain": "www.allbirds.com", // the host that answered, after any redirect
  "storeUrl": "https://www.allbirds.com",
  "input": "https://www.allbirds.com",

  "productId": 7218356060240,
  "handle": "mens-strider-explore",
  "url": "https://www.allbirds.com/products/mens-strider-explore",
  "title": "Men's Strider Explore - Natural Black (Dark Grey Sole)",
  "vendor": "Allbirds",
  "productType": "Shoes",
  "tags": ["DNAM BRANDS", "YCRF_mens-trail-shoes", "allbirds::gender => mens"],

  "description": "From exploring urban landscapes to tackling the trails (or both), this active shoe is here for it all…",
  "descriptionHtml": null,           // set when includeDescriptionHtml is on

  "price": 130,                      // cheapest variant
  "priceMax": 130,                   // dearest variant
  "compareAtPrice": null,            // the "was" price on the same variant, when there is one
  "currency": "USD",                 // read from the storefront — the feed has none
  "available": false,                // is anything in stock

  "variantCount": 13,
  "variants": [
    { "id": 41334293889104, "title": "8", "sku": "A11768M080", "price": 130,
      "compareAtPrice": null, "available": false, "options": { "Size": "8" },
      "grams": 919, "position": 1, "imageSrc": null }
  ],
  "options": [{ "name": "Size", "position": 1, "values": ["8", "8.5", "9", "…"] }],
  "images": [{ "src": "https://cdn.shopify.com/s/files/1/1104/4168/files/A11768….png?v=1759336475",
               "alt": null, "width": 4000, "height": 4000, "position": 1 }],
  "imageUrl": "https://cdn.shopify.com/s/files/1/1104/4168/files/A11768….png?v=1759336475",

  "publishedAt": "2026-08-26T10:22:14-07:00",
  "createdAt": "2025-09-10T12:47:47-07:00",
  "updatedAt": "2026-09-09T07:23:04-07:00",

  "collection": null,                // the collection handle, when one was walked
  "position": 2,                     // 1-based, in the order this run read the store

  "error": null,
  "errorType": null,
  "scrapedAt": "2026-09-09T15:31:04.512Z",
  "source": "www.allbirds.com",
  "sourceUrl": "https://www.allbirds.com/products.json?limit=250&page=1"
}
```

With `"rowPerVariant": true`, the same product arrives as thirteen rows carrying the same columns, each with its own `variantId`, `sku`, `price`, `available` and `option1`–`option3`. With `"includeCollections": true`, the store's collections follow as `rowType: "collection"` rows with `productsCount`, `handle` and `url` — up to `maxCollectionsPerStore`, which defaults to 250 because a large shop can hold several thousand of them.

### Use cases

- **Competitor price and assortment tracking** — run it daily, diff on `productId` and `updatedAt`, and you have every price move, every new drop and every discontinued line.
- **Stock and sell-out monitoring** — `available` per variant tells you which sizes sold out and when, which is the signal behind most resale and demand forecasting.
- **Discount and promotion watching** — `compareAtPrice > price` is a sale, per variant, with no guesswork about a badge on a page.
- **Catalogue and feed building** — a clean product feed for a marketplace, an affiliate site, a comparison engine, or a merchandising review, without asking each merchant for access.
- **Dropshipping and sourcing research** — vendor, product type, tags, weight and SKU across a list of suppliers in one schema.
- **Retrieval for a shopping agent** — one flat array of products with descriptions and prices, ready to embed or hand to a model.
- **Brand and market mapping** — feed a list of domains and let the diagnostic rows tell you which of them are Shopify stores at all.

### How it works, and why it keeps working

Every Shopify store publishes its own catalogue as JSON at `/products.json`, and its collection list at `/collections.json`. Those endpoints are part of the storefront — the same data the shop's own theme reads — and they are open to anyone with the URL. This Actor reads them and nothing else. There is no browser, no rendering, no per-product page fetch, and no Admin API token: **one request returns up to 250 whole products**, variants, images and all.

| Step | Request | What it gets |
|---|---|---|
| 1 | `/products.json?limit=250&page=1` | Up to 250 products — or the answer that this shop is not one |
| 1b | `/` (the storefront) | The store's currency, which the product feed does not publish |
| 2… | `/products.json?limit=250&page=N` | The next 250, until your cap or the end of the catalogue |
| C | `/collections.json?limit=250&page=N` | The collection list, when `includeCollections` is on |

Three details are where a naive version of this goes wrong, and all three came out of real captured responses:

- **The end of a catalogue is not "a page smaller than `limit`."** Asked for five products, one captured store answered with five through one proxy exit and **three** through another. A walker that stopped at "fewer than I asked for" would have read three products and called it a catalogue. So page one teaches the walk what this shop calls a full page, and every later page is measured against that. A shop that ignores `page` altogether is caught by remembering product IDs.
- **A 404 is not an answer to "is this Shopify?".** A store with its feed switched off — a headless storefront, a password-protected shop — answers exactly like a site that was never on Shopify. So the 404 body is inspected first, and only when it gives nothing away is the storefront read once and classified. You get `products-disabled` or `not-shopify`, not a shrug.
- **`www` is not a different shop.** Redirects settle most of it and are followed; when they do not, the other spelling of the host is tried. Whatever finally answers is the origin the rows name, so every `url` in your dataset is a link that opens.

Underneath: **Apify proxy**, one pinned session per parallel worker. When something in front of a shop refuses an exit IP — HTTP 403, HTTP 429, or a CDN block page wearing an HTTP 200 — that session is retired and **the same page is asked for once more from a different address**. Retrying on an address that was just refused only deepens the block, so it is never done. If the second address is refused too, the walk stops, **keeps every product it already delivered**, and files one free `blocked` row saying where it stopped.

Pages of the same store are spaced 250–600 ms apart. Nothing forces that; it is the difference between reading a catalogue and hammering a shop.

### How it compares

- **No app install, no store access.** Nothing to add to a merchant's admin, no Admin API token, no partner account. It reads what the storefront already serves the public.
- **One request per 250 products.** Not one page fetch per product. That is why the price per product can be what it is.
- **Variants are real rows, not a nested blob.** Turn on `rowPerVariant` and every SKU is a line with its own price and stock, so a spreadsheet or a SQL table can hold it without a JSON parser.
- **Failures are legible and free.** Not on Shopify, feed switched off, blocked, empty, a typo in a domain — each is a diagnostic row with an `errorType` you can branch on, and no charge. A run that returns nothing at all finishes **FAILED** with the reason in its status message, never a green run containing an apology.
- **A partial catalogue is kept, not thrown away.** Hit `maxRunSecs` or your charge ceiling on page 9 and you keep pages 1–8.
- **Prices are numbers.** `price`, `priceMax` and `compareAtPrice` are floats in the store's own currency, not strings with a symbol glued on.

### Input reference

| Field | Type | Default | What it does |
|---|---|---|---|
| `stores` **(required)** | array of strings | prefilled with one store | Domains or URLs. Bare domains, full URLs, `myshopify.com` domains, product links and collection links all work. Duplicates are read, and billed, once |
| `maxProductsPerStore` | integer | `250` | Products per store. Also sets the page size, so asking for 100 downloads 100. `0` = the whole catalogue. Counts products, not rows, when `rowPerVariant` is on |
| `rowPerVariant` | boolean | `false` | One row per variant instead of one per product. Variant rows are billed like product rows |
| `includeCollections` | boolean | `false` | Also return the store's collections as their own rows, billed at $0.0005 each |
| `maxCollectionsPerStore` | integer | `250` | Collection rows per store, when collections are on. Also the page size. `0` = every collection the store has — which on a big catalogue can be thousands |
| `collectionHandle` | string | — | Read one collection from every store instead of the whole catalogue. Handle, path or full URL. A `stores` entry that names its own collection keeps it |
| `includeDescriptionHtml` | boolean | `false` | Add the raw description markup next to the plain text every row already has |
| `includeImages` | boolean | `true` | Fill in the `images` array. Off still keeps `imageUrl`. Nothing extra is fetched either way |
| `onlyAvailable` | boolean | `false` | Skip sold-out products, and sold-out variants. Skipped products do not count towards the cap |
| `maxConcurrency` | integer | `3` | Stores in parallel. Each worker keeps its own proxy session. Pages within one store are sequential |
| `maxRunSecs` | integer | `240` | Whole-run wall-clock budget. When it runs out the Actor keeps what it has and files a free diagnostic row for each store it never reached |
| `proxyConfiguration` | object | Apify datacenter | The cheap default, which most stores accept. Switch it to the RESIDENTIAL group for the brands that sit behind a bot filter |

### Output reference

Every row carries the same keys. `ok: true` is a product, a variant or a collection; `ok: false` is a free diagnostic row.

| Field | What it is |
|---|---|
| `rowType` | `product`, `variant`, `collection` or `diagnostic` |
| `storeDomain`, `storeUrl`, `input` | The host that answered, its origin, and the entry you supplied |
| `productId`, `handle`, `url`, `title` | Shopify's ID, the URL slug, the public page, the name |
| `vendor`, `productType`, `tags` | The merchant's own brand, category and tags — often the only structured facts a shop publishes |
| `description`, `descriptionHtml` | The description as text (always) and as markup (opt-in) |
| `price`, `priceMax`, `compareAtPrice`, `currency` | Cheapest and dearest variant, the "was" price paired with `price`, and the store's currency |
| `available`, `variantCount`, `variants` | Whether anything is in stock, how many variants, and all of them |
| `options`, `images`, `imageUrl` | The option axes, every image, and the first one |
| `variantId`, `variantTitle`, `sku`, `option1`–`3`, `grams`, `variantImageUrl` | The variant, on `rowPerVariant` rows |
| `collection`, `collectionId`, `productsCount` | The collection walked, and the collection's own ID and size on collection rows |
| `publishedAt`, `createdAt`, `updatedAt` | The shop's own timestamps. `updatedAt` is the field to diff runs on |
| `position` | 1-based, in the order this run read the store |
| `ok`, `error`, `errorType` | Whether this row is data, and if not, why not |
| `scrapedAt`, `source`, `sourceUrl` | When, and the exact feed page it came from |

`errorType` on a diagnostic row is one of:

| Value | Meaning | Charged? |
|---|---|---|
| `not-shopify` | The domain answers, but it is not a Shopify store | No |
| `products-disabled` | A Shopify store whose public product feed is switched off — headless, password-protected, or turned off by the merchant | No |
| `blocked` | Something in front of the store refused two different proxy exits. Products already returned for that store are kept | No |
| `not-found` | Nothing answered at that domain — a typo, a dead shop, or DNS | No |
| `no-products` | The catalogue (or the collection) is published and empty | No |
| `invalid-input` | The entry was not a domain or a URL | No |
| `timeout` | The run's `maxRunSecs` budget ran out before this store was reached | No |

### Pricing

**$0.50 per 1,000 products.** Pay-per-event, with the proxy already inside that number — there is no separate proxy line on your bill for this Actor.

| Event | What triggers it | FREE | Starter | Scale | Business |
|---|---|---|---|---|---|
| **Product returned** *(primary)* | One product **or variant** row written to your dataset | $0.0005 | $0.0005 | $0.0004 | $0.0003 |
| Collection returned | One collection row, when you ask for them | $0.0005 | $0.0005 | $0.0005 | $0.0005 |
| Run started | Once per run, after the first product | $0.001 | $0.001 | $0.001 | $0.001 |

Collections are billed per row too, at $0.0005 each. Shops keep many more collections than most people expect — several thousand on a large catalogue, most of them merchandising rules rather than anything a shopper browses — so `includeCollections` is off by default and `maxCollectionsPerStore` caps it at 250 when you turn it on. Raise it deliberately.

**Worked example.** 20 stores at 250 products each, of which 2 domains turn out not to be Shopify:

- 18 stores × 250 products × $0.0005 = **$2.25**
- 1 run start = **$0.001**
- 2 non-Shopify domains = **$0.00**
- **Total: $2.251**

What you are never charged for: a domain that is not a Shopify store, a store with its feed switched off, an empty catalogue, a blocked store, a typo, or a store the run never reached before `maxRunSecs`. If a whole run comes back empty it finishes FAILED and bills **nothing at all**, start fee included.

Set `ACTOR_MAX_TOTAL_CHARGE_USD` on a run and the Actor stops walking once the ceiling is in sight, rather than handing you rows it cannot bill or billing you for rows it cannot hand over. It finishes SUCCEEDED with the ceiling named in its status message, and everything already delivered is yours.

### Limits, and the ones that might bite

**Not every store publishes the feed.** A merchant can switch `/products.json` off, and a *headless* store — one whose front end is built separately from Shopify — does not serve it at all. Those stores come back as free `products-disabled` rows. There is no other public endpoint that lists a store's catalogue, so this is a real limit rather than something a different tool solves quietly.

**Some storefronts refuse datacenter addresses.** Most large shops serve the feed to anyone, but a CDN rule in front of a storefront can refuse datacenter address ranges — sometimes only from certain exits, or only at certain times. When that happens you get free `blocked` rows rather than partial data, and switching `proxyConfiguration` to the RESIDENTIAL group is the fallback. That is why the proxy is a normal input and not a locked one.

**Stock is the merchant's flag, not a warehouse count.** `available` is Shopify's own boolean, which follows the shop's inventory policy: a store that lets customers buy past zero reports its products as available. There is no quantity in the public feed for any client, this one included.

**Currency comes from the storefront, not the feed.** Shopify's public product feed publishes prices as bare numbers with no currency anywhere in the payload, so the Actor reads the shop's front page once per store to get it. A shop whose front page could not be read leaves `currency` null and its prices in the store's own default currency.

**Alt text is usually missing.** The public feed's image objects carry `src`, `width`, `height` and `position` — and, on most stores, no alt text at all. The column is there and is filled when a store publishes one.

**Only what is published.** Draft products, products hidden from the online sales channel, and market- or customer-specific prices are not in the public feed. This Actor does not log in, does not accept a store password and does not take an API key belonging to anyone, and it never will — that is a deliberate line, not a missing feature.

**Prices are the shop's default market.** A store that shows different prices in different countries publishes its home-market prices here.

**The upstream format may change.** Shopify has served this feed in this shape for years, but it is a storefront endpoint and it can change without notice. When a shape changes, rows stop arriving and you get free `products-disabled` or `not-shopify` diagnostic rows rather than quietly wrong data, and a run that returns nothing bills nothing.

**Rate and reliability.** Requests go out through Apify proxy with per-worker sessions, one rotation per refusal, and a 250–600 ms pause between pages of the same store. Three stores in parallel is the default because it is where throughput and block rate balance; raising `maxConcurrency` speeds a long list up and makes blocks more likely.

### Use it from an AI agent, or from code

One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with **limited permissions**, uses **pay-per-event** pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The **Integrations** tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.

```bash
curl -X POST "https://api.apify.com/v2/acts/insight.solutions~shopify-products-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"stores":["allbirds.com"],"maxProductsPerStore":50,"onlyAvailable":true}'
```

```python
## pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/shopify-products-api").call(run_input={
    "stores": ["https://www.allbirds.com/collections/mens"],
    "maxProductsPerStore": 0,
    "rowPerVariant": True,
    "onlyAvailable": True,
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if not row.get("ok"):
        print("skipped:", row["input"], row["errorType"])
        continue
    sale = row["compareAtPrice"] and row["compareAtPrice"] > row["price"]
    print(f'{row["title"]} / {row["variantTitle"]}  {row["price"]} {row["currency"] or ""}'
          f'{"  ON SALE" if sale else ""}')
```

Turn `includeImages` off when you only need the words and the numbers — the payload shrinks and the price is the same.

### FAQ

**Do I need permission from the store?**
The endpoint this reads is part of every Shopify storefront and is served to anyone who asks, the same as the shop's own product pages. No app install, no token, no store password. Whether the *use* you make of a catalogue is allowed is a separate question, and it is yours — see the notes at the bottom.

**Why did my store come back as `products-disabled`?**
Either the merchant switched the public feed off, or the shop is headless — the front end is built separately and only the checkout is Shopify. Both are common on very large brands. There is no public endpoint that lists such a store's catalogue.

**How do I get every product from a big store?**
Set `maxProductsPerStore: 0` and raise `maxRunSecs`. Ten thousand products is 40 sequential requests with a short pause between each — plan for a minute or two, and bound it with `ACTOR_MAX_TOTAL_CHARGE_USD` if you are not sure how big the shop is.

**Are variant rows charged?**
Yes, at the same rate as products — a variant row is a row. That is why `rowPerVariant` is off by default. `variants[]` on a product row carries the same information for one charge.

**How fresh is it?**
Live. Every run reads the store at that moment; nothing is cached. `scrapedAt` is on every row, and `updatedAt` is the shop's own last-changed timestamp, which is what to diff between runs.

**Can I get inventory quantities?**
No, and neither can any other public client: the storefront feed publishes a boolean, not a count.

**Can I scrape one collection instead of the whole store?**
Yes — paste the collection URL into `stores`, or set `collectionHandle` to apply one handle across every store in the list.

**What happens if one store fails?**
The others still run. The failed one produces a free diagnostic row and the run finishes `SUCCEEDED`. If *every* store fails, the run finishes `FAILED` and you are billed nothing at all.

**Which proxy should I use?**
Start with the default datacenter proxy; it is what the price is built on and it works on the large brands we have measured. If a particular store comes back `blocked`, re-run that one with the RESIDENTIAL group — a refusal is usually about the exit address, not about the shop.

### Legal and data-protection notes

- **Public storefront data only.** Every source is an endpoint the shop serves to any visitor. The Actor never logs in, never accepts cookies or a store password, never takes an API key belonging to anyone else, and never touches draft, unpublished or admin-only data.
- **Product data is not personal data, but your use of it is regulated anyway.** Catalogues, prices and stock are business information, and collecting them for price comparison, market research and competitor analysis is ordinary and long-established. Reselling a merchant's descriptions and photography verbatim is a different thing — the copy and the images belong to whoever made them.
- **Respect the shop.** This Actor paces itself, identifies as an ordinary client and never tries to get past a refusal more than once. Please do not defeat that by pointing a thousand parallel runs at one store.
- **Not affiliated with Shopify Inc. or with any store, brand or merchant whose catalogue you retrieve.** All product names and trademarks belong to their respective owners and are used only to describe which public endpoints this Actor reads.

### Our other Actors

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

**Video, audio & social**

- [YouTube Transcript API](https://apify.com/insight.solutions/youtube-transcript-api) — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- [YouTube Comments API](https://apify.com/insight.solutions/youtube-comments-api) — comments and replies with likes, pinned and hearted flags, newest or top sort.
- [YouTube Channel API](https://apify.com/insight.solutions/youtube-channel-api) — a channel's videos, Shorts and live streams, plus YouTube search.
- [Podcast Search, Episodes & Charts API](https://apify.com/insight.solutions/podcast-api) — Apple Podcasts search, charts and full episode feeds.
- [Bluesky Scraper](https://apify.com/insight.solutions/bluesky-scraper) — profiles, posts, followers and follows from the public AT Protocol API.
- [Telegram Channel Scraper](https://apify.com/insight.solutions/telegram-channel-scraper) — posts, views and channel stats from public Telegram channels.
- [Substack Scraper](https://apify.com/insight.solutions/substack-scraper) — posts with full free text, comments and publication profiles.

**News, documents & the web**

- [Google News Search, Topics & Real Article URLs](https://apify.com/insight.solutions/google-news-api) — news search and topic feeds with the publisher's real URL decoded.
- [Website to Markdown — Content Extractor for LLMs & RAG](https://apify.com/insight.solutions/website-content-extractor) — any site as clean Markdown, text and heading-aware chunks.
- [Internet Archive API](https://apify.com/insight.solutions/internet-archive-api) — archive.org search, item metadata, files and reviews.
- [Wayback Machine Toolkit](https://apify.com/insight.solutions/wayback-toolkit) — archived URL inventories, snapshots and text diffs between dates.
- [Website Technology Detector](https://apify.com/insight.solutions/website-tech-detector) — the tech stack behind any site, with the evidence for each detection.
- [Domain Intelligence API](https://apify.com/insight.solutions/domain-intelligence-api) — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- [SEO Page Audit](https://apify.com/insight.solutions/seo-page-audit) — sitemap crawl with on-page checks, structured data and broken-link reports.
- [Keyword Suggestions API](https://apify.com/insight.solutions/keyword-suggestions-api) — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- [Website Contact Extractor](https://apify.com/insight.solutions/website-contact-extractor) — emails, phone numbers and social profiles from any list of websites.

**Business, finance & jobs**

- [Congress & Insider Trades API](https://apify.com/insight.solutions/congress-insider-trades-api) — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- [SEC EDGAR API](https://apify.com/insight.solutions/sec-edgar-api) — filings, XBRL financials and full-text search by ticker or CIK.
- [Y Combinator Companies, Batches & Founders](https://apify.com/insight.solutions/yc-companies-directory) — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- [Career Site Jobs API](https://apify.com/insight.solutions/ats-jobs-api) — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- [New Job Postings Monitor](https://apify.com/insight.solutions/job-postings-monitor) — new, closed and changed postings on the career sites you watch.

**Apps & games**

- [App Store & Google Play Reviews API](https://apify.com/insight.solutions/app-reviews-api) — reviews from both stores with ratings, versions and developer replies.
- [App Store Top Charts & App Search API](https://apify.com/insight.solutions/app-charts-api) — Apple top charts by country and genre, plus app search and details.
- [Steam Reviews API](https://apify.com/insight.solutions/steam-reviews-api) — Steam reviews with playtime, helpfulness and game details.
- [Steam Game Data API](https://apify.com/insight.solutions/steam-store-stats-api) — prices, tags, review scores, live player counts and top charts.

# Actor input Schema

## `stores` (type: `array`):

One entry per store. A bare domain (`allbirds.com`), a full URL (`https://www.allbirds.com`) and a `myshopify.com` domain all work, and so does a link to any page of the shop — a product page, a collection page, a link with tracking parameters glued on. A collection URL such as `https://www.allbirds.com/collections/mens` narrows that entry to that collection. Redirects are followed, so `example.com` and `www.example.com` are the same job written twice, and duplicates are read, and billed, once.

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

How many products to read from each store. Shopify serves up to 250 per request, so a smaller number here is also a smaller download: ask for 100 and the Actor asks the shop for 100. Set 0 for the whole catalogue, which on a large shop means thousands of rows and a run bounded only by your time and charge budgets. With `rowPerVariant` on this still counts products, not rows — one product can be twenty variants.

## `rowPerVariant` (type: `boolean`):

Return one row for every buyable variant — each size, colour and SKU on its own line, with `variantId`, `sku`, its own `price` and its own `available`, plus every product column repeated. Off, each product is one row and its variants ride along in the `variants` array. Variant rows are billed like product rows, and a shoe with thirteen sizes is thirteen of them, so this is off by default.

## `includeCollections` (type: `boolean`):

Also return one row per collection in the store — handle, title, description, product count and image — read from the shop's public collection list. These are `rowType: "collection"` rows and they are billed at $0.0005 each. A large shop can have thousands of collections, which is why this is off by default.

## `maxCollectionsPerStore` (type: `integer`):

How many collection rows to return per store, when `includeCollections` is on. Ignored otherwise. Large shops keep far more collections than anyone expects — three or four thousand on a big catalogue, most of them merchandising rules rather than anything a shopper sees — and each one is a billable row, so this is capped by default. It is also the page size, so asking for 40 downloads 40. Set 0 for every collection the store has.

## `collectionHandle` (type: `string`):

Read only one collection from every store, instead of the whole catalogue. Give the handle (`mens`), the path (`/collections/mens`) or the whole collection URL — all three work. A `stores` entry that already names a collection keeps its own; this applies to the ones that do not. Leave it empty for the full catalogue.

## `includeDescriptionHtml` (type: `boolean`):

Add `descriptionHtml`, the merchant's raw description markup, alongside the plain-text `description` that every row already carries. Themes and page-builder apps can make that markup many times the size of the words in it, so it is opt-in.

## `includeImages` (type: `boolean`):

Fill in the `images` array — every image of the product with its src, size and position. Turn it off and rows keep `imageUrl`, the first image, which is a link rather than a payload. Nothing extra is fetched either way; the images are already in the feed.

## `onlyAvailable` (type: `boolean`):

Skip products with nothing in stock — and, with `rowPerVariant` on, skip sold-out variants too. "In stock" is Shopify's own flag, so a shop that lets customers buy past zero reports its products as available. Skipped products do not count towards `maxProductsPerStore`.

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

How many stores are read at once. Each parallel worker keeps its own proxy session, so a store that gets blocked burns only its own exit IP. Pages *within* one store are sequential and spaced 250–600 ms apart, so this is the only lever on speed for a long list. Three is the balance that holds up in practice.

## `maxRunSecs` (type: `integer`):

Wall-clock budget for the whole run. When it is reached the Actor stops walking, keeps and bills for every product it already wrote, and files a free diagnostic row for each store it never reached. A store stopped part-way keeps its rows and is not marked as failed. Nothing is charged for work that did not happen.

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

Shopify serves `/products.json` to anyone, so this Actor defaults to Apify's datacenter proxy — the cheap one — and its cost is already inside the per-product price. Some large brands sit behind a bot filter that refuses datacenter address ranges; those come back as free `blocked` rows, and switching this to the RESIDENTIAL group is the fix. Sessions rotate automatically when an exit IP is refused.

## Actor input object example

```json
{
  "stores": [
    "allbirds.com",
    "https://www.allbirds.com/collections/mens"
  ],
  "maxProductsPerStore": 100,
  "rowPerVariant": false,
  "includeCollections": false,
  "maxCollectionsPerStore": 250,
  "collectionHandle": "mens",
  "includeDescriptionHtml": false,
  "includeImages": true,
  "onlyAvailable": false,
  "maxConcurrency": 3,
  "maxRunSecs": 240,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per product, or per variant when you ask for them, plus one row per collection when you ask for those. Stores that could not be read get a free diagnostic row saying why. Delivered as JSON items in the default dataset.

# 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 = {
    "stores": [
        "https://www.allbirds.com"
    ],
    "maxProductsPerStore": 100,
    "rowPerVariant": false,
    "includeCollections": false,
    "maxCollectionsPerStore": 250,
    "includeDescriptionHtml": false,
    "includeImages": true,
    "onlyAvailable": false,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("insight.solutions/shopify-products-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 = {
    "stores": ["https://www.allbirds.com"],
    "maxProductsPerStore": 100,
    "rowPerVariant": False,
    "includeCollections": False,
    "maxCollectionsPerStore": 250,
    "includeDescriptionHtml": False,
    "includeImages": True,
    "onlyAvailable": False,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("insight.solutions/shopify-products-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 '{
  "stores": [
    "https://www.allbirds.com"
  ],
  "maxProductsPerStore": 100,
  "rowPerVariant": false,
  "includeCollections": false,
  "maxCollectionsPerStore": 250,
  "includeDescriptionHtml": false,
  "includeImages": true,
  "onlyAvailable": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call insight.solutions/shopify-products-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,insight.solutions/shopify-products-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/OLEXMl1WQMIZwKkGa/builds/Fq73RRH1nkQ0pRONT/openapi.json
