Shopify Products Scraper — Descriptions, Specs & Tags avatar

Shopify Products Scraper — Descriptions, Specs & Tags

Pricing

from $1.55 / 1,000 results

Go to Apify Store
Shopify Products Scraper — Descriptions, Specs & Tags

Shopify Products Scraper — Descriptions, Specs & Tags

Shopify Products Scraper extracts detailed product data from Shopify stores. Collect product titles, prices, variants, SKUs, images, descriptions, and availability for eCommerce research, price tracking, and catalog automation.

Pricing

from $1.55 / 1,000 results

Rating

0.0

(0)

Developer

Scrapio

Scrapio

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

Shopify Products Scraper — Descriptions, Specs & Tags

Most Shopify product scrapers hand you body_html exactly as the merchant typed it and tags as one comma-joined string. This Shopify product scraper decomposes both: plain-text descriptions, bullet lists, headings and Label: value spec pairs pulled out of the description, plus a real key → values tag attribute map built from the shop's own tag taxonomy — four incompatible tag conventions handled in the same run. Every row comes back as typed JSON, ready to filter, pivot or push into a database without a parsing step. This guide covers every field it returns and how catalogue, PIM and research teams actually deploy it.


🧭 What does this Shopify Products Scraper do?

It reads one or more Shopify storefronts' public product catalogues and returns one row per product, decomposing three things a raw catalogue export leaves as unstructured text: the merchant's description, the tag taxonomy, and the fields (product_type, vendor) that Shopify's own admin makes optional. No Shopify account, API key, or store login is required — it reads the same storefront catalogue data a browser sees.

  • Extracts core product identity and pricing: title, vendor, product_type, price, compare_at_price, handle
  • Turns body_html into descriptionText, descriptionWordCount, descriptionBullets, descriptionHeadings, descriptionLinks, descriptionImageUrls
  • Pulls Label: value lines out of the description into descriptionSpecs
  • Parses the shop's tags into a tagAttributes key → values map plus a plainTags list, across four tag-naming conventions
  • Filters the catalogue on description content, description length, tag keys, tag attribute values, vendor and product type
  • Flags data-quality gaps explicitly: missingDescription, missingProductType, missingVendor, isUntagged
  • Reports whether each store's catalogue was read in full: total_found, catalogueComplete

⚡ Features & Capabilities

Three things set this apart from a plain catalogue export: description decomposition, tag taxonomy parsing, and content-aware filtering — all built from data the storefront already publishes, with no extra page loaded per product beyond the product record itself.

Core features

  • Description decompositiondescriptionText (clean plain text, HTML entities decoded, markup stripped), descriptionWordCount, descriptionBullets[], descriptionHeadings[], descriptionLinks[{text, url}], descriptionImageUrls[], plus structure flags hasBulletList, hasHeading, hasTable, hasEmbeddedImage, hasOutboundLink
  • Spec-pair extractiondescriptionSpecs[{label, value}] pulled from lines like Material: 100% organic cotton. A candidate line only qualifies as a label if it is 5 words or fewer, under 40 characters, contains a letter, and doesn't end in punctuation or look like a URL — so ordinary sentences aren't misread as specs
  • Tag taxonomy parsingtagAttributes (key → list of values), tagAttributeKeys[], plainTags[], tagFormatsSeen[]. Handles key:value, key:sub:value, namespace::key => value and key_value in the same run under the default auto setting. The underscore convention only fires on a lowercase key of 3+ characters followed by exactly one underscore, specifically so merchant grouping codes like YGroup_* or FULL_PRICE aren't misread as attributes
  • Content and taxonomy filtersdescriptionKeywords, excludeDescriptionKeywords, minDescriptionWords, requireTagKeys, tagAttributeFilters, vendors, productTypes, all combining with AND; every kept row lists which filters it matched in matchedFilters
  • Data-quality flags on every rowmissingDescription, missingProductType, missingVendor, isUntagged, so gaps in the catalogue are visible instead of silently returned as empty strings

💡 This Shopify Products Scraper within the Scrapio data stack

This actor covers Shopify products — descriptions, specs and tag attributes. For merchant-level shipping and currency data on a Shopify store, use Shopify Merchant Scraper. For related-product and price-comparison data on a Shopify storefront, use Shopify Scraper. For a store's contact details — emails, phone numbers and social links — use Shopify Store Scraper. All three sit alongside this one in the Scrapio Shopify lineup and can be run against the same store list.


Why do developers and data teams scrape Shopify product catalogues?

🏢 Ecommerce, PIM and merchandising teams

Feed a list of competitor or supplier storefronts into shopDomains and get back descriptionSpecs as real {label, value} pairs instead of re-typing them out of marketing copy, and tagAttributes as a structured taxonomy instead of a comma string to regex against. productTypes and tagAttributeFilters narrow a run to one category before it ever reaches your PIM import script. missingProductType and isUntagged flag which SKUs need manual categorisation before a migration, rather than surfacing as silent blanks after import.

📊 AI training data and RAG indexing

descriptionText is markup-free, entity-decoded plain text — the highest-information field for embedding into a RAG index of product knowledge, and descriptionSpecs gives structured attribute facts a model can cite directly rather than re-extract from prose. For training data, tagAttributes and descriptionSpecs are the most consistently shaped fields across a run: same key structure whether the source tag was Type:CORE TRUNK or storefront:category:Womens Shoes. Both feed a Q&A or attribute-extraction dataset without a normalisation pass.

📱 Competitive and market intelligence

Run the same shopDomains list on a schedule and track price, compare_at_price and tagAttributes over time to see when a competitor discounts, relaunches under a new season tag, or adds a merchandising attribute you don't yet use. descriptionWordCount and descriptionSpecCount turn "their copy looks more detailed" into a number you can chart across a catalogue.

🔬 Research and academic use

Public product-description and tag-taxonomy data across many storefronts supports research into ecommerce merchandising conventions, description-quality variation, or category-taxonomy practices — scoped to what each storefront already publishes publicly, with no account or login involved.

🎥 Product and SaaS development

descriptionSpecs and tagAttributes are structured enough to sit behind a catalogue-enrichment API, a competitor-monitoring dashboard, or a directory product without a scraping-and-parsing layer of your own. full_data carries the complete raw product record alongside the parsed fields, so a field this actor doesn't parse out separately is still available.


🍚 Input Parameters

Read directly from the Actor's input schema. Nothing is required — leave everything but a store list on its default.

ParameterTypeDescriptionExample Value
shopDomainsarrayOne or more Shopify storefronts. Accepts a full URL (https://rothys.com), a bare hostname (rothys.com) or a myshopify domain (example.myshopify.com). Every store is processed in turn. Takes priority over startUrls when both are filled.["https://www.tentree.com", "https://rothys.com"]
startUrlsarrayThe original store-URL field, kept so an existing input file keeps working unchanged. Used only when shopDomains is empty.["https://rothys.com"]
parseDescriptionbooleanTurn the merchant's description into plain text plus structure fields. Off skips description parsing entirely, and the description filters below then have nothing to work on. Default true.true
descriptionOutputenumtext (plain text only) · structure (text + bullets, headings, links, images) · full (text + structure + spec pairs). Default "full"."full"
extractSpecPairsbooleanPull Label: value lines out of the description into descriptionSpecs. Only applies when descriptionOutput is "full". Default true.true
specLabelSeparatorsarrayCharacters that separate a spec label from its value. A colon binds directly to the label (Fit: Oversized); a dash or pipe must stand alone between spaces. Default `[":", "-", ""]`.
keepDescriptionHtmlbooleanAdd a descriptionHtml column holding the description exactly as published, alongside the parsed fields. Adds several kilobytes per row. Default false.false
parseTagAttributesbooleanSplit the shop's tags into a key/value attribute map (tagAttributes) and an unstructured list (plainTags). Off returns tags as a plain array only. Default true.true
tagFormatenumauto (all four conventions, recommended) · pair (key:value only) · three (key:sub:value only) · arrow (namespace::key => value only) · underscore (key_value only) · none (no parsing). Default "auto"."auto"
mergeTagKeyCasebooleanMerge tag keys that differ only by case (Category: and category:) into one lower-case key whose value list is the union of both. Default false.false
emitPlainTagsbooleanReturn the plainTags list of tags with no key/value structure. plainTagCount is returned either way, so attributeTagCount + plainTagCount always equals tagCount. Default true.true
descriptionKeywordsarrayKeep only products whose description contains at least one of these phrases (case-insensitive). Empty means no keyword filter.["organic cotton"]
excludeDescriptionKeywordsarrayDrop products whose description contains any of these phrases (case-insensitive).["gift card"]
minDescriptionWordsintegerKeep only products whose description reaches this many words; minimum 0. Products with no description are dropped when this is above 0. Default 0.20
requireTagKeysarrayKeep only products whose tag taxonomy contains every one of these keys (case-insensitive).["storefront:category"]
tagAttributeFiltersarrayKeep only products whose tag taxonomy holds a specific value, written as key=value. Several entries combine with AND. Case-insensitive.["Type=CORE TRUNK"]
vendorsarrayKeep only products from these vendors (case-insensitive exact match).["Rothy's"]
productTypesarrayKeep only products with one of these product types (case-insensitive exact match). Many shops leave product_type blank and put the category in a tag instead — use the tag filters above for those.["Sneakers"]
maxProductsPerShopintegerHow many products to read per store; 0 means the whole catalogue; minimum 0. Input-form default (prefill) is 50. If the field is omitted entirely — for example when the input is built programmatically without it — the Actor falls back to 100.50
maxItemsintegerThe original per-store limit, kept for input-file compatibility. Used only when maxProductsPerShop is not set. Minimum 0.50
proxyConfigurationobjectWhich proxies to use. Default is no proxy. If a store does not answer on the default route, the run automatically escalates to Apify Proxy and, if that also fails, to a residential proxy group.{ "useApifyProxy": false }
{
"shopDomains": ["https://www.tentree.com", "https://rothys.com"],
"parseDescription": true,
"descriptionOutput": "full",
"extractSpecPairs": true,
"specLabelSeparators": [":", "-", "|"],
"keepDescriptionHtml": false,
"parseTagAttributes": true,
"tagFormat": "auto",
"mergeTagKeyCase": false,
"emitPlainTags": true,
"descriptionKeywords": ["organic cotton"],
"minDescriptionWords": 20,
"requireTagKeys": ["storefront:category"],
"tagAttributeFilters": ["Type=CORE TRUNK"],
"maxProductsPerShop": 50,
"proxyConfiguration": { "useApifyProxy": false }
}

Supported URL types and input formats

  • Full URL: https://rothys.com or https://www.tentree.com — scheme and any www. prefix are both accepted and normalised
  • Bare hostname: rothys.com — a scheme is added automatically
  • Myshopify domain: example.myshopify.com — read directly, no redirect resolution needed
  • Duplicate stores in the list are read once. A store the Actor cannot reach — password-gated, unavailable, or answering with something that isn't a catalogue payload — is skipped with a logged reason and produces no rows for that store, never a row claiming zero products

📦 Output Format

Typed JSON, one row per product, pushed to the dataset the moment that product is ready — no waiting for the whole store to finish. Field presence depends on input: description fields only appear when parseDescription is on, spec pairs only when descriptionOutput is "full" and extractSpecPairs is on, descriptionHtml only when keepDescriptionHtml is on. Download the dataset as JSON, CSV, Excel, XML or RSS from the Apify Console or API — standard for every Apify dataset.

Output for a product row

{
"store_url": "https://chubbiesshorts.com",
"product_url": "https://chubbiesshorts.com/products/the-domingos-7-inch-lined-classic-swim-trunk",
"product_id": 7856421273694,
"handle": "the-domingos-7-inch-lined-classic-swim-trunk",
"title": "The Domingos 7\" (Lined Classic Swim Trunk)",
"vendor": "Chubbies",
"product_type": null,
"price": "69.50",
"compare_at_price": null,
"tags": ["Type:CORE TRUNK", "Fabric:4-WAY STRETCH", "Season:SS26", "ALLSWIM"],
"tagCount": 4,
"tagAttributes": {
"Type": ["CORE TRUNK"],
"Fabric": ["4-WAY STRETCH"],
"Season": ["SS26"]
},
"tagAttributeKeys": ["Fabric", "Season", "Type"],
"attributeTagCount": 3,
"plainTags": ["ALLSWIM"],
"plainTagCount": 1,
"tagFormatsSeen": ["key:value"],
"tagAttributeCoverage": 1.0,
"descriptionText": "Lined Classic Swim Trunk\nOur original swim trunk...\nInseam: 7 inches",
"descriptionWordCount": 63,
"descriptionHtmlLength": 812,
"descriptionIsEmpty": false,
"descriptionBullets": ["Quick-dry 4-way stretch fabric", "Boxer-brief mesh liner"],
"descriptionHeadings": [],
"descriptionLinks": [],
"descriptionImageUrls": [],
"hasBulletList": true,
"hasHeading": false,
"hasTable": false,
"hasEmbeddedImage": false,
"hasOutboundLink": false,
"descriptionSpecs": [{ "label": "Inseam", "value": "7 inches" }],
"descriptionSpecCount": 1,
"missingDescription": false,
"missingProductType": true,
"missingVendor": false,
"isUntagged": false,
"matchedFilters": [],
"total_found": 250,
"catalogueComplete": false,
"scrapedAt": "2026-08-08T00:00:00Z",
"full_data": { "product": { "…": "the complete raw product record from the storefront" } }
}
FieldDescription
store_url, product_url, product_id, handleProduct identity
title, vendor, product_typeCore product fields; product_type/vendor are null when the shop leaves them blank, never an empty string
price, compare_at_priceFirst variant's prices, exactly as the shop publishes them
tagsReal array of tags, normalised whether the source returned an array or a comma-joined string
tagCount, attributeTagCount, plainTagCountattributeTagCount + plainTagCount always equals tagCount; all three are null when the shop publishes no tag field at all, or parseTagAttributes is off
tagAttributesKey → list-of-values map parsed from the tags
tagAttributeKeysSorted attribute keys present on this product
plainTagsTags carrying no key/value structure (present when emitPlainTags is on)
tagFormatsSeenWhich of the four conventions this product's tags used
tagAttributeCoverageFraction of this store's read catalogue publishing any tag attribute — the same value on every row from that store
descriptionText, descriptionWordCount, descriptionHtmlLengthClean plain text and its size; text/word-count are null when there is no description
descriptionIsEmptytrue when the description renders no visible text
descriptionBullets, descriptionHeadingsList items and headings, in document order (present when descriptionOutput is "structure" or "full")
descriptionLinks, descriptionImageUrlsOutbound links ({text, url}) and <img> sources inside the description
hasBulletList, hasHeading, hasTable, hasEmbeddedImage, hasOutboundLinkStructure flags
descriptionSpecs, descriptionSpecCount{label, value} pairs found in the description (present when descriptionOutput is "full" and extractSpecPairs is on)
descriptionHtmlThe description exactly as published (present only when keepDescriptionHtml is on)
missingDescription, missingProductType, missingVendor, isUntaggedCatalogue gap flags
matchedFiltersWhich active filters this row satisfied
total_foundHow many products the catalogue walk had read by the time it stopped for this store — the true catalogue size only when catalogueComplete is true
catalogueCompleteWhether the walk stopped because the catalogue ran out (true), or because of a product cap, page ceiling, or a read error (false)
scrapedAtISO-8601 UTC timestamp of when this row was built
full_dataThe complete raw product record — {"product": {...}} — so a field not parsed out separately is still available

Schema stability and export options

Field names in this output are the Actor's own — descriptionSpecs, tagAttributes and the rest are constructed by this Actor's parser, not passed through raw from Shopify, so they stay stable even if a store's theme changes how it renders body_html. full_data carries Shopify's raw product JSON verbatim underneath, as a fallback for anything the parser doesn't surface as its own column. Export the dataset as JSON, CSV, Excel (XLSX), XML or RSS directly from the Apify Console, or pull it programmatically with the Apify API or apify_client.


💡 Shopify Products Scraper Strategy Guide

🎯 Strategy 1: Real-time enrichment pipeline

Trigger a run when a new SKU or store enters your pipeline — a webhook, a queue message, or a manual API call. Run this Actor against that one shopDomains entry with a small maxProductsPerShop, then read descriptionSpecs, tagAttributes and the gap flags (missingProductType, isUntagged) off the dataset and write them back into your PIM record. Because the Actor pushes each row as soon as that product is parsed, a single-store enrichment run can be consumed before the whole run finishes.

🎯 Strategy 2: Scheduled monitoring and alerting

Run the same shopDomains list on an Apify Schedule — daily or weekly. Compare each new run's price, compare_at_price and tagAttributes against the previous run on product_url as the join key, and alert on the deltas that matter: a price drop, a new Season tag, or a product that went from tagged to isUntagged. scrapedAt timestamps each comparison point.

🎯 Strategy 3: Bulk dataset build

List every store you need in shopDomains (or split across parallel runs, one store list per run) and set maxProductsPerShop to 0 for the whole catalogue. Within a store, product detail pages are fetched with bounded concurrency rather than all at once, and the catalogue walk stops at a fixed page ceiling per store (see Honest limitations below) — plan very large single-store pulls with that in mind. Aggregate the resulting dataset to CSV or load it into a database for research or training-set construction.

Strategy comparison at a glance

StrategyBest forRun patternOutput format
Real-time enrichmentPIM / catalogue-record enrichmentOn-demand trigger, single store, small maxProductsPerShopDataset row consumed via API as it's pushed
Scheduled monitoringPrice and tag-taxonomy change trackingApify Schedule, recurring run over a fixed store listDataset per run, diffed externally on product_url
Bulk dataset buildResearch or training-set constructionMany shopDomains, or parallel runs across storesDataset exported to CSV/JSON

ScraperWhat it extracts
Shopify Merchant ScraperA Shopify store's currency and shipping configuration
Shopify ScraperRelated products and price comparison across a Shopify storefront
Shopify Store ScraperA store's contact details — emails, phone numbers and social links
Amazon Product Details & Variant ASINs ScraperAmazon product detail pages and their variant ASINs
eBay Product Listing ScrapereBay listing item specifics, condition and photos
Amazon Price TrackerAmazon price history, deals, coupons and lowest recorded price

How to integrate this Shopify Products Scraper with your stack

This Actor works with any language or tool that can make an HTTP request through the Apify API, or the official apify_client / apify-client SDKs.

Python

import csv
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run_input = {
"shopDomains": ["https://www.tentree.com", "https://rothys.com"],
"maxProductsPerShop": 100,
"descriptionOutput": "full",
}
run = client.actor("YOUR_USERNAME/shopify-products-scraper-descriptions-specs-tags").call(
run_input=run_input
)
rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())
with open("shopify_products.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["store_url", "title", "vendor", "price", "descriptionSpecCount", "tagAttributeKeys"])
for row in rows:
writer.writerow([
row.get("store_url"),
row.get("title"),
row.get("vendor"),
row.get("price"),
row.get("descriptionSpecCount"),
",".join(row.get("tagAttributeKeys") or []),
])
print(f"Wrote {len(rows)} products to shopify_products.csv")

Node.js

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const runInput = {
shopDomains: ['https://www.tentree.com', 'https://rothys.com'],
maxProductsPerShop: 100,
descriptionOutput: 'full',
};
const run = await client
.actor('YOUR_USERNAME/shopify-products-scraper-descriptions-specs-tags')
.call(runInput);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const row of items) {
console.log(row.title, row.price, row.tagAttributeKeys);
}

Async and scheduled pipelines

For a large multi-store pull, start the run and poll it rather than waiting synchronously — client.actor(...).call() blocks until the run finishes, or use start() and check client.run(runId).get() on your own interval. For recurring pulls, use an Apify Schedule against a fixed input, and read the resulting dataset from your own job once the run completes; the Apify platform can also fire a webhook on run completion for a fire-and-forget large job.


🎯 Who needs a Shopify product, description and tag scraper?

🏢 Ecommerce, PIM and merchandising teams

Import descriptionSpecs directly as structured attribute pairs into a product feed or PIM instead of re-keying them from marketing copy, and use missingProductType/isUntagged to flag the SKUs a migration needs to touch by hand.

📊 AI/ML and RAG engineering teams

Index descriptionText for retrieval and cite descriptionSpecs as structured facts in generated answers, without a separate HTML-cleaning step in the ingestion pipeline.

📱 Competitive intelligence and market research analysts

Track price, compare_at_price and tagAttributes for a fixed store list over time to see discounting patterns and merchandising-attribute changes as they happen, not after the fact.

🔬 Researchers

Public description and tag-taxonomy data across many storefronts, scoped to what each store already publishes — suited to academic or market research into ecommerce merchandising practices.

🎥 SaaS and product builders

tagAttributes and descriptionSpecs are structured enough to sit behind a catalogue-enrichment API or competitor-monitoring product without building a scraper first.


Scraping publicly accessible data is generally lawful in the United States — courts have held that accessing data a website makes available without a login is not a computer-fraud violation (hiQ Labs, Inc. v. LinkedIn Corp., 9th Cir. 2019, on remand 2022). That precedent concerned a social platform, but its core holding — that scraping public pages is not "unauthorized access" — is widely cited for public-data scraping generally.

Two separate questions apply here. Scraping public data is generally legal; violating a site's Terms of Service is a matter of civil contract risk between the scraper and the site operator, not a criminal one. This Actor returns product and business-record data — titles, descriptions, prices, vendors and merchant-assigned tags — not personal data about individuals, so data-protection frameworks like GDPR and CCPA, which govern personal data, do not attach to its output. Product catalogue content itself may carry copyright or database-rights protection in some jurisdictions, which is a separate question from scraping method.

This Shopify Products Scraper returns only publicly accessible data. What you do with that data is your responsibility — consult legal counsel for commercial applications, especially where a store's own Terms of Service restrict automated access.


❓ Frequently asked questions

Does this Shopify Products Scraper work without a Shopify account?

Yes. It reads the same public storefront catalogue data a browser sees — no Shopify account, API key, or store login is required or accepted as input.

How does it handle Shopify's anti-scraping measures?

By default it connects directly. When a store returns a retriable status (429, 403, 503, and similar), the connection automatically escalates from a direct connection to Apify Proxy, and then to a residential proxy group if that still fails — each attempt with a growing backoff delay and some randomised jitter, up to four attempts per request. A status that can never succeed (400, 401, 404, 410) is treated as terminal and not retried.

Can I run it at scale without getting blocked?

The retry-and-escalation behaviour above applies per request. There is no published concurrency or rate-limit figure beyond what's documented here: product detail pages within one store are fetched with bounded concurrency rather than all at once, and each store's catalogue walk is capped at a fixed number of pages (see Honest limitations).

How fresh is the data this Actor returns?

Live. Every run re-fetches the storefront's current catalogue JSON — nothing is cached or served from a previous run.

Which fields work best for AI training and RAG indexing?

For RAG, descriptionText is the high-information field — clean plain text with markup and entities stripped. For training data, tagAttributes and descriptionSpecs are the most consistently shaped fields across a run, since both are built to the same {key: [values]} and {label, value} structure regardless of which tag convention or spec-separator the merchant used. All fields return as typed primitives — strings, numbers, booleans, arrays, or null — needing no further normalisation.

Does this Actor collect personal data?

No. It returns product and catalogue fields — titles, descriptions, prices, vendors and tags — that Shopify stores publish as part of their public product listings, not data about individuals.

Does it work with Claude, ChatGPT, and other AI agent tools?

It's callable as an HTTP endpoint by any agent framework through the Apify API — a run returns typed JSON that needs no parsing before it's placed in a model's context window.

Can I get one row per variant or SKU?

Not with this Actor. It returns one row per product, with price and compare_at_price taken from the first variant only.

What is a "spec pair"?

A Label: value line the merchant wrote inside the description — for example Material: 100% organic cotton, Compatibility: Handheld Bidet Sprayer. Returned as {label, value} objects in descriptionSpecs so you can pivot on them without re-parsing prose.

Two shops use Category: and category: — will they collide?

Only if you ask them to. By default they stay two separate keys exactly as written. Set mergeTagKeyCase to true to merge them into one lower-case key whose value list is the union of both.


ℹ️ Honest limitations

  • ⚠️ Each store's catalogue walk is capped. The Actor reads up to 40 pages of 250 products per page — 10,000 products per store — before it stops regardless of how much catalogue remains. When that happens, catalogueComplete is false and total_found reflects only what was read, not the store's true product count.
  • One row per product, not per variant. price and compare_at_price come from the first variant only; there is no per-SKU output.
  • Not every shop publishes a tag taxonomy. A store with no structured tags returns tagAttributes: {} and tagAttributeCoverage: 0.0 — never an invented key.
  • Description structure is theme-dependent. Whether a product's description carries bullet lists, headings, or spec-parseable lines depends entirely on how that store's theme and merchant wrote it; the per-row structure flags tell you which is which rather than assuming a shape.
  • maxProductsPerShop's effective default can differ from its input-form default. The form pre-fills 50; if the field is left out of the input object entirely, the Actor falls back to 100.
  • Nothing is invented. A value the shop doesn't publish comes back as null, never as 0 or an empty string standing in for a real measurement, and attributeTagCount + plainTagCount equals tagCount on every row so a parsing gap is visible rather than hidden.
  • A store that isn't reachable produces no rows and a logged reason — password-gated, unavailable, or an unreadable payload — never a row claiming the store has zero products.
  • No AI, no model calls. Every field is produced by deterministic text and string parsing.

Disclaimer

This Shopify Products Scraper extracts only publicly available data from Shopify storefronts. This tool is intended for lawful use cases only. Users are responsible for complying with each store's terms of service and applicable data protection laws in their jurisdiction.