{
  "data": {
    "id": "rfdRrnEMOeqFiOPHE",
    "actId": "MaKZQdevxgpAmpJ8l",
    "userId": "IYgBDiXtHOijAaUel",
    "startedAt": "2026-10-27T12:01:11.120Z",
    "finishedAt": "2026-10-27T12:01:14.619Z",
    "status": "SUCCEEDED",
    "meta": {
      "origin": "CLI"
    },
    "stats": {
      "durationMillis": 3366,
      "runTimeSecs": 3.366,
      "computeUnits": 0.00374,
      "imageSizeBytes": 357808128
    },
    "options": {
      "useCache": true,
      "betaPackages": false,
      "memoryMbytes": 4096,
      "diskMbytes": 8192
    },
    "createdByOrganizationMemberUserId": "QYo53EZkJk0prePWY",
    "exitCode": 0,
    "inputSchema": "{\n  \"title\": \"Shopify Store Catalog & Pricing Extractor Input\",\n  \"description\": \"Scrape one or many Shopify storefronts: full catalog, collection, single product, in-store search, or the collection directory.\",\n  \"type\": \"object\",\n  \"schemaVersion\": 1,\n  \"properties\": {\n    \"mode\": {\n      \"title\": \"Mode\",\n      \"type\": \"string\",\n      \"description\": \"What to scrape. `catalog` walks the public product feed, `collection` walks specific collections, `product` fetches exact product handles, `search` uses the storefront search suggestions, `collections` lists collection metadata, and `auto` detects the type of each URL in `startUrls`.\",\n      \"editor\": \"select\",\n      \"enum\": [\n        \"catalog\",\n        \"collection\",\n        \"product\",\n        \"search\",\n        \"collections\",\n        \"auto\"\n      ],\n      \"enumTitles\": [\n        \"Full store catalog\",\n        \"Specific collections\",\n        \"Specific products\",\n        \"In-store search\",\n        \"Collection directory\",\n        \"Auto-detect from URLs\"\n      ],\n      \"default\": \"catalog\",\n      \"prefill\": \"catalog\"\n    },\n    \"storeUrls\": {\n      \"title\": \"Shopify store URLs\",\n      \"type\": \"array\",\n      \"description\": \"Store domains or URLs, e.g. `deathwishcoffee.com` or `https://www.allbirds.com`. Accepts custom domains and `*.myshopify.com`. A product or collection URL is reduced to its store for catalog/collections/search modes.\",\n      \"editor\": \"stringList\",\n      \"prefill\": [\n        \"https://deathwishcoffee.com\"\n      ]\n    },\n    \"startUrls\": {\n      \"title\": \"Start URLs (mixed, optional)\",\n      \"type\": \"array\",\n      \"description\": \"Store, collection or product URLs. In `auto` mode each URL selects its own job: `/products/<handle>` fetches one product, `/collections/<handle>` fetches a collection, any other store URL fetches the catalog. In the other modes these URLs add stores and, for collection/product modes, their handles.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"collectionHandles\": {\n      \"title\": \"Collection handles (mode=collection)\",\n      \"type\": \"array\",\n      \"description\": \"Collection handles or collection URLs, e.g. `new-arrivals`, `sale`, or `https://store.com/collections/sale`. Handles are applied to every store in `storeUrls`; a full collection URL is applied to its own store.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"productHandles\": {\n      \"title\": \"Product handles (mode=product)\",\n      \"type\": \"array\",\n      \"description\": \"Product handles or product URLs, e.g. `dark-roast-coffee` or `https://store.com/products/dark-roast-coffee`. Handles are applied to every store in `storeUrls`; a full product URL is applied to its own store.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"searchQuery\": {\n      \"title\": \"Search query (mode=search)\",\n      \"type\": \"string\",\n      \"description\": \"In-store search term. Shopify's public suggest endpoint returns at most 10 products per store and has no pagination.\",\n      \"editor\": \"textfield\"\n    },\n    \"currency\": {\n      \"title\": \"Currency\",\n      \"type\": \"string\",\n      \"description\": \"ISO 4217 code to pin prices to, e.g. `USD`, `EUR`, `GBP`. Use `store` to use the store's base currency from `/meta.json`. Pinning is important because Shopify localizes prices by IP.\",\n      \"editor\": \"textfield\",\n      \"default\": \"store\",\n      \"prefill\": \"USD\"\n    },\n    \"country\": {\n      \"title\": \"Country (optional)\",\n      \"type\": \"string\",\n      \"description\": \"Two-letter ISO country code passed as `?country=` to the storefront, e.g. `US`, `GB`, `DE`.\",\n      \"editor\": \"textfield\"\n    },\n    \"vendor\": {\n      \"title\": \"Vendor contains\",\n      \"type\": \"string\",\n      \"description\": \"Keep products whose vendor contains this text (case-insensitive).\",\n      \"editor\": \"textfield\"\n    },\n    \"productType\": {\n      \"title\": \"Product type contains\",\n      \"type\": \"string\",\n      \"description\": \"Keep products whose product type contains this text (case-insensitive).\",\n      \"editor\": \"textfield\"\n    },\n    \"tag\": {\n      \"title\": \"Tag contains\",\n      \"type\": \"string\",\n      \"description\": \"Keep products with at least one tag containing this text (case-insensitive).\",\n      \"editor\": \"textfield\"\n    },\n    \"minPrice\": {\n      \"title\": \"Minimum price\",\n      \"type\": \"number\",\n      \"description\": \"Drop products whose lowest variant price is below this value.\",\n      \"minimum\": 0,\n      \"maximum\": 1000000\n    },\n    \"maxPrice\": {\n      \"title\": \"Maximum price\",\n      \"type\": \"number\",\n      \"description\": \"Drop products whose lowest variant price is above this value.\",\n      \"minimum\": 0,\n      \"maximum\": 1000000\n    },\n    \"available\": {\n      \"title\": \"Availability\",\n      \"type\": \"string\",\n      \"description\": \"Keep all products, only products with at least one purchasable variant, or only sold-out products.\",\n      \"editor\": \"select\",\n      \"enum\": [\n        \"any\",\n        \"available\",\n        \"unavailable\"\n      ],\n      \"enumTitles\": [\n        \"Any availability\",\n        \"In stock only\",\n        \"Sold out only\"\n      ],\n      \"default\": \"any\",\n      \"prefill\": \"any\"\n    },\n    \"maxItems\": {\n      \"title\": \"Max records\",\n      \"type\": \"integer\",\n      \"description\": \"Hard cap on dataset rows across all stores. When `flattenVariants` is on, each variant row counts as one.\",\n      \"minimum\": 1,\n      \"maximum\": 50000,\n      \"default\": 1000,\n      \"prefill\": 3\n    },\n    \"maxPages\": {\n      \"title\": \"Max pages per feed\",\n      \"type\": \"integer\",\n      \"description\": \"Page cap per product/collection feed. Each page holds up to 250 items and Shopify refuses offsets above 25,000, so the effective maximum is 100.\",\n      \"minimum\": 1,\n      \"maximum\": 100,\n      \"default\": 100,\n      \"prefill\": 100\n    },\n    \"includeDetails\": {\n      \"title\": \"Include barcode, weight and inventory\",\n      \"type\": \"boolean\",\n      \"description\": \"Fetch `/products/{handle}.json` for each emitted product to add barcode, weight/grams and inventory fields. Costs one extra request per product and is off by default. Product mode always uses the detail endpoint.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"flattenVariants\": {\n      \"title\": \"One row per variant\",\n      \"type\": \"boolean\",\n      \"description\": \"Emit one `recordType: variant` row per variant instead of one product row with nested variants. Variant and product IDs are de-duplicated.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"proxyConfiguration\": {\n      \"title\": \"Proxy configuration (optional)\",\n      \"type\": \"object\",\n      \"description\": \"Leave empty to fetch directly. After a 403 or 429 block the actor retries through Apify's datacenter proxy (AUTO) automatically. If you set this, your configuration is used for that retry instead of AUTO; direct requests are still tried first.\",\n      \"editor\": \"proxy\"\n    }\n  },\n  \"required\": [\n    \"mode\"\n  ]\n}",
    "readme": "# Shopify Store Catalog & Pricing Extractor\r\n\r\nTurn any public Shopify storefront into a structured product feed. Point the actor at one store or a list of stores, choose a mode, and get prices, compare-at prices, variants, stock status, images, options, collection metadata and optional barcode/weight/inventory detail.\r\n\r\nNo Shopify account, admin API key, cookie or login is required. The actor reads the same public storefront JSON that a shopper's browser receives. Proxy setup is also optional: requests go direct first, and the actor switches to Apify's datacenter proxy automatically only after a real 403 or 429 block.\r\n\r\n## Modes\r\n\r\n| Mode | What it does | Source endpoint |\r\n|---|---|---|\r\n| `catalog` | Full published product feed for every store | `/products.json?limit=250&page=N` |\r\n| `collection` | Products from one or more collection handles/URLs | `/collections/{handle}/products.json` |\r\n| `product` | Exact products by handle or product URL | `/products/{handle}.json` |\r\n| `search` | In-store product search, up to 10 results per store | `/search/suggest.json` |\r\n| `collections` | Collection directory with titles, handles and reported counts | `/collections.json?limit=250&page=N` |\r\n| `auto` | Mixed `startUrls`; each URL selects its own job | Detected per URL |\r\n\r\n`auto` treats `/products/<handle>` as a product job, `/collections/<handle>` as a collection job, `/collections` as a collection-directory job, and any other store URL as a catalog job.\r\n\r\n## Input\r\n\r\n| Field | Type | Default | Notes |\r\n|---|---|---|---|\r\n| `mode` | select | `catalog` | One of the six modes above. |\r\n| `storeUrls` | string list | - | Store domains or full URLs; `*.myshopify.com` is supported. The prefill uses `deathwishcoffee.com`. |\r\n| `startUrls` | string list | empty | Mixed store/product/collection URLs. Used mainly by `auto`. |\r\n| `collectionHandles` | string list | empty | Handles or collection URLs for `mode=collection`. |\r\n| `productHandles` | string list | empty | Handles or product URLs for `mode=product`. |\r\n| `searchQuery` | string | empty | Required for `mode=search`. |\r\n| `currency` | string | `store` | ISO 4217 code such as `USD` or `EUR`; `store` uses `/meta.json`. |\r\n| `country` | string | empty | Optional ISO-2 country passed as `?country=`. |\r\n| `vendor` | string | empty | Case-insensitive vendor substring filter. |\r\n| `productType` | string | empty | Case-insensitive product-type substring filter. |\r\n| `tag` | string | empty | Keep products with a tag containing this substring. |\r\n| `minPrice` | number | empty | Drop products below this lowest-variant price. |\r\n| `maxPrice` | number | empty | Drop products above this lowest-variant price. |\r\n| `available` | select | `any` | `any`, `available` or `unavailable`. |\r\n| `maxItems` | integer | `1000` | 1-50,000 dataset rows; variant rows count individually when flattening. |\r\n| `maxPages` | integer | `100` | 1-100 pages per feed. Shopify refuses offsets above 25,000. |\r\n| `includeDetails` | boolean | `false` | One extra `/products/{handle}.json` request per product for barcode, weight and inventory. |\r\n| `flattenVariants` | boolean | `false` | Emit one `recordType: variant` row per variant instead of nested product rows. |\r\n| `proxyConfiguration` | object | empty | Direct requests are always tried first. After a 403/429 the actor retries via AUTO, or via this configuration if provided. |\r\n\r\nThe smallest useful run is also the daily prefill: `deathwishcoffee.com`, three records, no detail enrichment.\r\n\r\n### Examples\r\n\r\nFull catalog from one store:\r\n\r\n```json\r\n{\r\n  \"mode\": \"catalog\",\r\n  \"storeUrls\": [\"https://deathwishcoffee.com\"],\r\n  \"currency\": \"USD\",\r\n  \"maxItems\": 100\r\n}\r\n```\r\n\r\nCollection and sale filters:\r\n\r\n```json\r\n{\r\n  \"mode\": \"collection\",\r\n  \"storeUrls\": [\"allbirds.com\"],\r\n  \"collectionHandles\": [\"mens-sale\"],\r\n  \"maxPrice\": 100,\r\n  \"available\": \"available\",\r\n  \"flattenVariants\": true\r\n}\r\n```\r\n\r\nExact products by URL:\r\n\r\n```json\r\n{\r\n  \"mode\": \"product\",\r\n  \"startUrls\": [\r\n    \"https://deathwishcoffee.com/products/valhalla-java\",\r\n    \"https://deathwishcoffee.com/products/dark-roast\"\r\n  ],\r\n  \"includeDetails\": true\r\n}\r\n```\r\n\r\nSearch a set of stores:\r\n\r\n```json\r\n{\r\n  \"mode\": \"search\",\r\n  \"storeUrls\": [\"allbirds.com\", \"rothys.com\"],\r\n  \"searchQuery\": \"wool runner\",\r\n  \"maxItems\": 20\r\n}\r\n```\r\n\r\nMixed URLs in one run:\r\n\r\n```json\r\n{\r\n  \"mode\": \"auto\",\r\n  \"startUrls\": [\r\n    \"https://allbirds.com\",\r\n    \"https://allbirds.com/collections/mens\",\r\n    \"https://allbirds.com/products/wool-runner\"\r\n  ]\r\n}\r\n```\r\n\r\n## Product output\r\n\r\nProduct records use `recordType: \"product\"` and contain:\r\n\r\n- `productId` and legacy `id`, `handle`, `title`, `url`\r\n- `vendor`, `productType`, `tags`\r\n- `bodyHtml` (raw) and `description` (plain text)\r\n- `createdAt`, `updatedAt`, `publishedAt`, `publishedScope`\r\n- `price`, `priceMin`, `priceMax`, `compareAtPrice`, `compareAtPriceMax`, `discountPercent`, `onSale`, `currency`\r\n- `available`, `variantCount`, `availableVariantCount`\r\n- `variants`: variant id, title, SKU, price, compare-at price, availability, option values and named `options`\r\n- `images` (URL list), `imageDetails` (URL/width/height/position), `featuredImage`\r\n- `options`: option names and their values\r\n- `store`: `{url, name, currency}`, plus legacy `storeUrl`\r\n- `source`, `sourceUrl`, `scrapedAt`, `recordType`\r\n\r\nWith `includeDetails` enabled, variants can also contain `barcode`, `weight`, `weightUnit`, `grams`, `inventoryQuantity`, `inventoryPolicy`, `inventoryManagement`, `quantityRule`, `requiresShipping` and `taxable`. Inventory quantities are only emitted when the store exposes `inventory_management` and the value is not Shopify's `999999` untracked sentinel.\r\n\r\nWhen the detail endpoint omits the `available` flag but publishes inventory management, availability is derived from stock: positive quantity, or `inventory_policy=continue`, means available. Without published inventory the flag stays omitted rather than being guessed.\r\n\r\n## Variant output\r\n\r\nWith `flattenVariants: true`, each variant becomes its own row with `recordType: \"variant\"`, plus the parent `productId`, `productHandle`, `productTitle`, `productUrl`, `vendor` and `productType`. Product and variant IDs are de-duplicated.\r\n\r\n## Collection output\r\n\r\n`mode=collections` emits `recordType: \"collection\"` rows with `collectionId`, `handle`, `url`, `title`, `description`, `productsCount` (the store's reported count), timestamps, image, `store`, `sourceUrl` and `scrapedAt`.\r\n\r\n## Run summary\r\n\r\nThe dataset stays clean: products, variants and collections only. A separate key-value record named `OUTPUT` holds the run summary with each store's status, HTTP code, pages fetched, record counts, detail failures and capped flag.\r\n\r\nPer-store statuses:\r\n\r\n| Status | Meaning |\r\n|---|---|\r\n| `OK` | The feed was read normally. |\r\n| `CAPPED` | The page cap or Shopify's 25,000-offset ceiling was reached. |\r\n| `PARTIAL` | Some pages or detail requests failed after data was collected. |\r\n| `PASSWORD_PROTECTED` | The store returned 401. |\r\n| `FROZEN` | The store returned 402. |\r\n| `BLOCKED` | The store returned 403, including after the automatic proxy retry. |\r\n| `NO_JSON` | The store returned 404 or a non-JSON response. |\r\n| `RATE_LIMITED` | The store returned 429 after retries. |\r\n\r\nIf every store fails, the run fails with the per-store map instead of finishing silently with an empty dataset. If some stores succeed, the run succeeds and the summary reports the failures.\r\n\r\n## Pagination notes\r\n\r\n- 250 products per page, the Shopify maximum.\r\n- Short pages are not the end of the feed. The walker continues until an empty page, an error, `maxPages`, or the 25,000-offset ceiling.\r\n- Products are de-duplicated by store and product ID; variants by store and variant ID.\r\n- `maxItems` is a global cap across all stores and jobs.\r\n\r\n## Currency\r\n\r\nShopify localizes prices by IP. The actor pins `?currency=` on every list, collection and product request. The default `store` setting reads the store's base currency from `/meta.json`; an explicit ISO code overrides it. Each product records the currency reported by its variants when available.\r\n\r\n## FAQ\r\n\r\n**Do I need a Shopify account or API key?** No. The actor only reads public storefront JSON.\r\n\r\n**Do I need a proxy?** No. Direct requests are tried first. Apify's datacenter proxy is used only after a 403 or 429.\r\n\r\n**Why does search return at most 10 products per store?** That is Shopify's public suggest endpoint limit. There is no pagination on it. Use `catalog` or `collection` mode for larger feeds.\r\n\r\n**Why is the product count lower than the collection page says?** Collection product endpoints expose only purchasable/published products, while `products_count` counts more. The reported count is kept as `productsCount` and is not treated as ground truth.\r\n\r\n**Why are some inventory fields missing?** Inventory is a merchant setting. Stores that do not publish `inventory_quantity` cannot be forced to reveal it, and the untracked `999999` sentinel is never emitted as stock.\r\n\r\n**What happens on password-protected stores?** Their public JSON returns 401. The actor reports `PASSWORD_PROTECTED` and does not retry with a proxy.\r\n\r\n**Why did a large store stop at 25,000 products?** Shopify rejects page offsets above 25,000 on `/products.json`. Split large catalogs by collection to go deeper.\r\n\r\n**Are descriptions HTML or plain text?** Both are available: `bodyHtml` keeps the original markup and `description` is a plain-text version.\r\n\r\n**Is this affiliated with Shopify?** No. It is an independent third-party tool that reads publicly available storefront data.\r\n\r\n## Limits\r\n\r\n- Password-protected stores are not scrapable through the public JSON.\r\n- Stores that disable the JSON endpoints return `NO_JSON`.\r\n- Collection membership is not exposed publicly; the actor emits collection records instead of inventing per-product membership.\r\n- Search is capped at 10 products per store.\r\n- Inventory counts are available only where the store publishes them.\r\n- The product feed is limited to the newest 25,000 products per store/feed.\r\n",
    "actorDefinition": {
      "actorSpecification": 1,
      "name": "shopify-store-catalog-and-pricing-extractor",
      "title": "Shopify Store Catalog & Pricing Extractor",
      "description": "Export products, variants, collections, and prices from any public Shopify store. Multi-store catalog, collection, product, search, and collections modes; optional barcode, weight, and inventory enrichment. No login or API key required.",
      "version": "2.0",
      "buildTag": "latest",
      "readme": "# Shopify Store Catalog & Pricing Extractor\r\n\r\nTurn any public Shopify storefront into a structured product feed. Point the actor at one store or a list of stores, choose a mode, and get prices, compare-at prices, variants, stock status, images, options, collection metadata and optional barcode/weight/inventory detail.\r\n\r\nNo Shopify account, admin API key, cookie or login is required. The actor reads the same public storefront JSON that a shopper's browser receives. Proxy setup is also optional: requests go direct first, and the actor switches to Apify's datacenter proxy automatically only after a real 403 or 429 block.\r\n\r\n## Modes\r\n\r\n| Mode | What it does | Source endpoint |\r\n|---|---|---|\r\n| `catalog` | Full published product feed for every store | `/products.json?limit=250&page=N` |\r\n| `collection` | Products from one or more collection handles/URLs | `/collections/{handle}/products.json` |\r\n| `product` | Exact products by handle or product URL | `/products/{handle}.json` |\r\n| `search` | In-store product search, up to 10 results per store | `/search/suggest.json` |\r\n| `collections` | Collection directory with titles, handles and reported counts | `/collections.json?limit=250&page=N` |\r\n| `auto` | Mixed `startUrls`; each URL selects its own job | Detected per URL |\r\n\r\n`auto` treats `/products/<handle>` as a product job, `/collections/<handle>` as a collection job, `/collections` as a collection-directory job, and any other store URL as a catalog job.\r\n\r\n## Input\r\n\r\n| Field | Type | Default | Notes |\r\n|---|---|---|---|\r\n| `mode` | select | `catalog` | One of the six modes above. |\r\n| `storeUrls` | string list | - | Store domains or full URLs; `*.myshopify.com` is supported. The prefill uses `deathwishcoffee.com`. |\r\n| `startUrls` | string list | empty | Mixed store/product/collection URLs. Used mainly by `auto`. |\r\n| `collectionHandles` | string list | empty | Handles or collection URLs for `mode=collection`. |\r\n| `productHandles` | string list | empty | Handles or product URLs for `mode=product`. |\r\n| `searchQuery` | string | empty | Required for `mode=search`. |\r\n| `currency` | string | `store` | ISO 4217 code such as `USD` or `EUR`; `store` uses `/meta.json`. |\r\n| `country` | string | empty | Optional ISO-2 country passed as `?country=`. |\r\n| `vendor` | string | empty | Case-insensitive vendor substring filter. |\r\n| `productType` | string | empty | Case-insensitive product-type substring filter. |\r\n| `tag` | string | empty | Keep products with a tag containing this substring. |\r\n| `minPrice` | number | empty | Drop products below this lowest-variant price. |\r\n| `maxPrice` | number | empty | Drop products above this lowest-variant price. |\r\n| `available` | select | `any` | `any`, `available` or `unavailable`. |\r\n| `maxItems` | integer | `1000` | 1-50,000 dataset rows; variant rows count individually when flattening. |\r\n| `maxPages` | integer | `100` | 1-100 pages per feed. Shopify refuses offsets above 25,000. |\r\n| `includeDetails` | boolean | `false` | One extra `/products/{handle}.json` request per product for barcode, weight and inventory. |\r\n| `flattenVariants` | boolean | `false` | Emit one `recordType: variant` row per variant instead of nested product rows. |\r\n| `proxyConfiguration` | object | empty | Direct requests are always tried first. After a 403/429 the actor retries via AUTO, or via this configuration if provided. |\r\n\r\nThe smallest useful run is also the daily prefill: `deathwishcoffee.com`, three records, no detail enrichment.\r\n\r\n### Examples\r\n\r\nFull catalog from one store:\r\n\r\n```json\r\n{\r\n  \"mode\": \"catalog\",\r\n  \"storeUrls\": [\"https://deathwishcoffee.com\"],\r\n  \"currency\": \"USD\",\r\n  \"maxItems\": 100\r\n}\r\n```\r\n\r\nCollection and sale filters:\r\n\r\n```json\r\n{\r\n  \"mode\": \"collection\",\r\n  \"storeUrls\": [\"allbirds.com\"],\r\n  \"collectionHandles\": [\"mens-sale\"],\r\n  \"maxPrice\": 100,\r\n  \"available\": \"available\",\r\n  \"flattenVariants\": true\r\n}\r\n```\r\n\r\nExact products by URL:\r\n\r\n```json\r\n{\r\n  \"mode\": \"product\",\r\n  \"startUrls\": [\r\n    \"https://deathwishcoffee.com/products/valhalla-java\",\r\n    \"https://deathwishcoffee.com/products/dark-roast\"\r\n  ],\r\n  \"includeDetails\": true\r\n}\r\n```\r\n\r\nSearch a set of stores:\r\n\r\n```json\r\n{\r\n  \"mode\": \"search\",\r\n  \"storeUrls\": [\"allbirds.com\", \"rothys.com\"],\r\n  \"searchQuery\": \"wool runner\",\r\n  \"maxItems\": 20\r\n}\r\n```\r\n\r\nMixed URLs in one run:\r\n\r\n```json\r\n{\r\n  \"mode\": \"auto\",\r\n  \"startUrls\": [\r\n    \"https://allbirds.com\",\r\n    \"https://allbirds.com/collections/mens\",\r\n    \"https://allbirds.com/products/wool-runner\"\r\n  ]\r\n}\r\n```\r\n\r\n## Product output\r\n\r\nProduct records use `recordType: \"product\"` and contain:\r\n\r\n- `productId` and legacy `id`, `handle`, `title`, `url`\r\n- `vendor`, `productType`, `tags`\r\n- `bodyHtml` (raw) and `description` (plain text)\r\n- `createdAt`, `updatedAt`, `publishedAt`, `publishedScope`\r\n- `price`, `priceMin`, `priceMax`, `compareAtPrice`, `compareAtPriceMax`, `discountPercent`, `onSale`, `currency`\r\n- `available`, `variantCount`, `availableVariantCount`\r\n- `variants`: variant id, title, SKU, price, compare-at price, availability, option values and named `options`\r\n- `images` (URL list), `imageDetails` (URL/width/height/position), `featuredImage`\r\n- `options`: option names and their values\r\n- `store`: `{url, name, currency}`, plus legacy `storeUrl`\r\n- `source`, `sourceUrl`, `scrapedAt`, `recordType`\r\n\r\nWith `includeDetails` enabled, variants can also contain `barcode`, `weight`, `weightUnit`, `grams`, `inventoryQuantity`, `inventoryPolicy`, `inventoryManagement`, `quantityRule`, `requiresShipping` and `taxable`. Inventory quantities are only emitted when the store exposes `inventory_management` and the value is not Shopify's `999999` untracked sentinel.\r\n\r\nWhen the detail endpoint omits the `available` flag but publishes inventory management, availability is derived from stock: positive quantity, or `inventory_policy=continue`, means available. Without published inventory the flag stays omitted rather than being guessed.\r\n\r\n## Variant output\r\n\r\nWith `flattenVariants: true`, each variant becomes its own row with `recordType: \"variant\"`, plus the parent `productId`, `productHandle`, `productTitle`, `productUrl`, `vendor` and `productType`. Product and variant IDs are de-duplicated.\r\n\r\n## Collection output\r\n\r\n`mode=collections` emits `recordType: \"collection\"` rows with `collectionId`, `handle`, `url`, `title`, `description`, `productsCount` (the store's reported count), timestamps, image, `store`, `sourceUrl` and `scrapedAt`.\r\n\r\n## Run summary\r\n\r\nThe dataset stays clean: products, variants and collections only. A separate key-value record named `OUTPUT` holds the run summary with each store's status, HTTP code, pages fetched, record counts, detail failures and capped flag.\r\n\r\nPer-store statuses:\r\n\r\n| Status | Meaning |\r\n|---|---|\r\n| `OK` | The feed was read normally. |\r\n| `CAPPED` | The page cap or Shopify's 25,000-offset ceiling was reached. |\r\n| `PARTIAL` | Some pages or detail requests failed after data was collected. |\r\n| `PASSWORD_PROTECTED` | The store returned 401. |\r\n| `FROZEN` | The store returned 402. |\r\n| `BLOCKED` | The store returned 403, including after the automatic proxy retry. |\r\n| `NO_JSON` | The store returned 404 or a non-JSON response. |\r\n| `RATE_LIMITED` | The store returned 429 after retries. |\r\n\r\nIf every store fails, the run fails with the per-store map instead of finishing silently with an empty dataset. If some stores succeed, the run succeeds and the summary reports the failures.\r\n\r\n## Pagination notes\r\n\r\n- 250 products per page, the Shopify maximum.\r\n- Short pages are not the end of the feed. The walker continues until an empty page, an error, `maxPages`, or the 25,000-offset ceiling.\r\n- Products are de-duplicated by store and product ID; variants by store and variant ID.\r\n- `maxItems` is a global cap across all stores and jobs.\r\n\r\n## Currency\r\n\r\nShopify localizes prices by IP. The actor pins `?currency=` on every list, collection and product request. The default `store` setting reads the store's base currency from `/meta.json`; an explicit ISO code overrides it. Each product records the currency reported by its variants when available.\r\n\r\n## FAQ\r\n\r\n**Do I need a Shopify account or API key?** No. The actor only reads public storefront JSON.\r\n\r\n**Do I need a proxy?** No. Direct requests are tried first. Apify's datacenter proxy is used only after a 403 or 429.\r\n\r\n**Why does search return at most 10 products per store?** That is Shopify's public suggest endpoint limit. There is no pagination on it. Use `catalog` or `collection` mode for larger feeds.\r\n\r\n**Why is the product count lower than the collection page says?** Collection product endpoints expose only purchasable/published products, while `products_count` counts more. The reported count is kept as `productsCount` and is not treated as ground truth.\r\n\r\n**Why are some inventory fields missing?** Inventory is a merchant setting. Stores that do not publish `inventory_quantity` cannot be forced to reveal it, and the untracked `999999` sentinel is never emitted as stock.\r\n\r\n**What happens on password-protected stores?** Their public JSON returns 401. The actor reports `PASSWORD_PROTECTED` and does not retry with a proxy.\r\n\r\n**Why did a large store stop at 25,000 products?** Shopify rejects page offsets above 25,000 on `/products.json`. Split large catalogs by collection to go deeper.\r\n\r\n**Are descriptions HTML or plain text?** Both are available: `bodyHtml` keeps the original markup and `description` is a plain-text version.\r\n\r\n**Is this affiliated with Shopify?** No. It is an independent third-party tool that reads publicly available storefront data.\r\n\r\n## Limits\r\n\r\n- Password-protected stores are not scrapable through the public JSON.\r\n- Stores that disable the JSON endpoints return `NO_JSON`.\r\n- Collection membership is not exposed publicly; the actor emits collection records instead of inventing per-product membership.\r\n- Search is capped at 10 products per store.\r\n- Inventory counts are available only where the store publishes them.\r\n- The product feed is limited to the newest 25,000 products per store/feed.\r\n",
      "input": {
        "title": "Shopify Store Catalog & Pricing Extractor Input",
        "description": "Scrape one or many Shopify storefronts: full catalog, collection, single product, in-store search, or the collection directory.",
        "type": "object",
        "schemaVersion": 1,
        "properties": {
          "mode": {
            "title": "Mode",
            "type": "string",
            "description": "What to scrape. `catalog` walks the public product feed, `collection` walks specific collections, `product` fetches exact product handles, `search` uses the storefront search suggestions, `collections` lists collection metadata, and `auto` detects the type of each URL in `startUrls`.",
            "editor": "select",
            "enum": [
              "catalog",
              "collection",
              "product",
              "search",
              "collections",
              "auto"
            ],
            "enumTitles": [
              "Full store catalog",
              "Specific collections",
              "Specific products",
              "In-store search",
              "Collection directory",
              "Auto-detect from URLs"
            ],
            "default": "catalog",
            "prefill": "catalog"
          },
          "storeUrls": {
            "title": "Shopify store URLs",
            "type": "array",
            "description": "Store domains or URLs, e.g. `deathwishcoffee.com` or `https://www.allbirds.com`. Accepts custom domains and `*.myshopify.com`. A product or collection URL is reduced to its store for catalog/collections/search modes.",
            "editor": "stringList",
            "prefill": [
              "https://deathwishcoffee.com"
            ]
          },
          "startUrls": {
            "title": "Start URLs (mixed, optional)",
            "type": "array",
            "description": "Store, collection or product URLs. In `auto` mode each URL selects its own job: `/products/<handle>` fetches one product, `/collections/<handle>` fetches a collection, any other store URL fetches the catalog. In the other modes these URLs add stores and, for collection/product modes, their handles.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "collectionHandles": {
            "title": "Collection handles (mode=collection)",
            "type": "array",
            "description": "Collection handles or collection URLs, e.g. `new-arrivals`, `sale`, or `https://store.com/collections/sale`. Handles are applied to every store in `storeUrls`; a full collection URL is applied to its own store.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "productHandles": {
            "title": "Product handles (mode=product)",
            "type": "array",
            "description": "Product handles or product URLs, e.g. `dark-roast-coffee` or `https://store.com/products/dark-roast-coffee`. Handles are applied to every store in `storeUrls`; a full product URL is applied to its own store.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "searchQuery": {
            "title": "Search query (mode=search)",
            "type": "string",
            "description": "In-store search term. Shopify's public suggest endpoint returns at most 10 products per store and has no pagination.",
            "editor": "textfield"
          },
          "currency": {
            "title": "Currency",
            "type": "string",
            "description": "ISO 4217 code to pin prices to, e.g. `USD`, `EUR`, `GBP`. Use `store` to use the store's base currency from `/meta.json`. Pinning is important because Shopify localizes prices by IP.",
            "editor": "textfield",
            "default": "store",
            "prefill": "USD"
          },
          "country": {
            "title": "Country (optional)",
            "type": "string",
            "description": "Two-letter ISO country code passed as `?country=` to the storefront, e.g. `US`, `GB`, `DE`.",
            "editor": "textfield"
          },
          "vendor": {
            "title": "Vendor contains",
            "type": "string",
            "description": "Keep products whose vendor contains this text (case-insensitive).",
            "editor": "textfield"
          },
          "productType": {
            "title": "Product type contains",
            "type": "string",
            "description": "Keep products whose product type contains this text (case-insensitive).",
            "editor": "textfield"
          },
          "tag": {
            "title": "Tag contains",
            "type": "string",
            "description": "Keep products with at least one tag containing this text (case-insensitive).",
            "editor": "textfield"
          },
          "minPrice": {
            "title": "Minimum price",
            "type": "number",
            "description": "Drop products whose lowest variant price is below this value.",
            "minimum": 0,
            "maximum": 1000000
          },
          "maxPrice": {
            "title": "Maximum price",
            "type": "number",
            "description": "Drop products whose lowest variant price is above this value.",
            "minimum": 0,
            "maximum": 1000000
          },
          "available": {
            "title": "Availability",
            "type": "string",
            "description": "Keep all products, only products with at least one purchasable variant, or only sold-out products.",
            "editor": "select",
            "enum": [
              "any",
              "available",
              "unavailable"
            ],
            "enumTitles": [
              "Any availability",
              "In stock only",
              "Sold out only"
            ],
            "default": "any",
            "prefill": "any"
          },
          "maxItems": {
            "title": "Max records",
            "type": "integer",
            "description": "Hard cap on dataset rows across all stores. When `flattenVariants` is on, each variant row counts as one.",
            "minimum": 1,
            "maximum": 50000,
            "default": 1000,
            "prefill": 3
          },
          "maxPages": {
            "title": "Max pages per feed",
            "type": "integer",
            "description": "Page cap per product/collection feed. Each page holds up to 250 items and Shopify refuses offsets above 25,000, so the effective maximum is 100.",
            "minimum": 1,
            "maximum": 100,
            "default": 100,
            "prefill": 100
          },
          "includeDetails": {
            "title": "Include barcode, weight and inventory",
            "type": "boolean",
            "description": "Fetch `/products/{handle}.json` for each emitted product to add barcode, weight/grams and inventory fields. Costs one extra request per product and is off by default. Product mode always uses the detail endpoint.",
            "default": false,
            "prefill": false
          },
          "flattenVariants": {
            "title": "One row per variant",
            "type": "boolean",
            "description": "Emit one `recordType: variant` row per variant instead of one product row with nested variants. Variant and product IDs are de-duplicated.",
            "default": false,
            "prefill": false
          },
          "proxyConfiguration": {
            "title": "Proxy configuration (optional)",
            "type": "object",
            "description": "Leave empty to fetch directly. After a 403 or 429 block the actor retries through Apify's datacenter proxy (AUTO) automatically. If you set this, your configuration is used for that retry instead of AUTO; direct requests are still tried first.",
            "editor": "proxy"
          }
        },
        "required": [
          "mode"
        ]
      },
      "output": {
        "actorOutputSchemaVersion": 1,
        "title": "Shopify Store Catalog & Pricing Extractor Output",
        "description": "Dataset rows for products, variants or collections, plus a per-store run summary record.",
        "type": "object",
        "properties": {
          "items": {
            "type": "string",
            "title": "Shopify records",
            "description": "Dataset containing product, variant and collection records (filtered by recordType).",
            "template": "{{links.apiDefaultDatasetUrl}}/items"
          },
          "runSummary": {
            "type": "string",
            "title": "Per-store run summary",
            "description": "Key-value store record named OUTPUT with per-store OK/CAPPED/PARTIAL and error statuses.",
            "template": "{{links.apiDefaultKeyValueStoreUrl}}/keys"
          }
        }
      },
      "dockerfile": "../Dockerfile",
      "storages": {
        "dataset": {
          "actorSpecification": 1,
          "views": {
            "overview": {
              "title": "Products",
              "transformation": {
                "fields": [
                  "recordType",
                  "title",
                  "price",
                  "currency",
                  "compareAtPrice",
                  "available",
                  "vendor",
                  "productType",
                  "storeUrl",
                  "url"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "title": {
                    "label": "Title",
                    "format": "text"
                  },
                  "price": {
                    "label": "Price",
                    "format": "number"
                  },
                  "currency": {
                    "label": "Currency",
                    "format": "text"
                  },
                  "compareAtPrice": {
                    "label": "Compare at",
                    "format": "number"
                  },
                  "available": {
                    "label": "Available",
                    "format": "text"
                  },
                  "vendor": {
                    "label": "Vendor",
                    "format": "text"
                  },
                  "productType": {
                    "label": "Type",
                    "format": "text"
                  },
                  "storeUrl": {
                    "label": "Store",
                    "format": "link"
                  },
                  "url": {
                    "label": "Product URL",
                    "format": "link"
                  }
                }
              }
            },
            "variants": {
              "title": "Variants",
              "transformation": {
                "fields": [
                  "recordType",
                  "productTitle",
                  "title",
                  "sku",
                  "price",
                  "compareAtPrice",
                  "currency",
                  "available",
                  "barcode",
                  "inventoryQuantity",
                  "storeUrl",
                  "productUrl"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "productTitle": {
                    "label": "Product",
                    "format": "text"
                  },
                  "title": {
                    "label": "Variant",
                    "format": "text"
                  },
                  "sku": {
                    "label": "SKU",
                    "format": "text"
                  },
                  "price": {
                    "label": "Price",
                    "format": "number"
                  },
                  "compareAtPrice": {
                    "label": "Compare at",
                    "format": "number"
                  },
                  "available": {
                    "label": "Available",
                    "format": "text"
                  },
                  "barcode": {
                    "label": "Barcode",
                    "format": "text"
                  },
                  "inventoryQuantity": {
                    "label": "Inventory",
                    "format": "number"
                  },
                  "productUrl": {
                    "label": "Product URL",
                    "format": "link"
                  }
                }
              }
            },
            "collections": {
              "title": "Collections",
              "transformation": {
                "fields": [
                  "recordType",
                  "title",
                  "handle",
                  "productsCount",
                  "storeUrl",
                  "url",
                  "scrapedAt"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "title": {
                    "label": "Collection",
                    "format": "text"
                  },
                  "handle": {
                    "label": "Handle",
                    "format": "text"
                  },
                  "productsCount": {
                    "label": "Reported products",
                    "format": "number"
                  },
                  "storeUrl": {
                    "label": "Store",
                    "format": "link"
                  },
                  "url": {
                    "label": "Collection URL",
                    "format": "link"
                  }
                }
              }
            },
            "detailed": {
              "title": "Detailed",
              "transformation": {
                "fields": [
                  "recordType",
                  "productId",
                  "id",
                  "handle",
                  "title",
                  "url",
                  "vendor",
                  "productType",
                  "tags",
                  "description",
                  "createdAt",
                  "updatedAt",
                  "publishedAt",
                  "publishedScope",
                  "price",
                  "priceMin",
                  "priceMax",
                  "compareAtPrice",
                  "compareAtPriceMax",
                  "discountPercent",
                  "onSale",
                  "currency",
                  "available",
                  "variantCount",
                  "availableVariantCount",
                  "variants",
                  "images",
                  "imageDetails",
                  "featuredImage",
                  "options",
                  "collectionHandle",
                  "store",
                  "storeUrl",
                  "source",
                  "sourceUrl",
                  "scrapedAt"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "productId": {
                    "label": "Product ID",
                    "format": "text"
                  },
                  "title": {
                    "label": "Title",
                    "format": "text"
                  },
                  "url": {
                    "label": "Product URL",
                    "format": "link"
                  },
                  "price": {
                    "label": "Price",
                    "format": "number"
                  },
                  "currency": {
                    "label": "Currency",
                    "format": "text"
                  },
                  "available": {
                    "label": "Available",
                    "format": "text"
                  },
                  "storeUrl": {
                    "label": "Store",
                    "format": "link"
                  },
                  "sourceUrl": {
                    "label": "Source",
                    "format": "link"
                  },
                  "scrapedAt": {
                    "label": "Scraped at",
                    "format": "text"
                  }
                }
              }
            }
          }
        }
      },
      "environmentVariables": {}
    },
    "buildNumber": "2.0.2",
    "imageDigest": "43bc4231b80013b3e7c6e9a338f5f7bbf9de5bc42891528dcf2f586adf8a4681",
    "actVersion": {
      "sourceType": "SOURCE_FILES",
      "buildTag": "latest",
      "versionNumber": "2.0"
    },
    "usage": {
      "ACTOR_COMPUTE_UNITS": 0.00374
    }
  }
}