Key takeaways
- Almost every Shopify storefront serves its catalogue as JSON at
/products.json, no key required. - Default 30 products per page, 250 maximum. Paginate until you get an empty array.
- Stock is a boolean, not a number. Meta titles, metafields and collections are absent.
- An empty response usually means bot protection or a merchant-level block, not that the store is not Shopify.
If you have ever wondered how price comparison sites, market research tools and migration services read Shopify catalogues so quickly, this is the answer. It is not clever. Shopify simply publishes the data.
What the endpoint is
Every Shopify storefront serves a JSON representation of its published products at a predictable path:
https://example-store.myshopify.com/products.json
https://www.example.com/products.json
Both forms work, because a custom domain is a front for the same storefront. No API key, no OAuth, no account.
This is worth being precise about, because two very different things share the name "Shopify API":
| products.json | Admin API | |
|---|---|---|
| Authentication | None | Access token required |
| Who can use it | Anyone | The store owner and apps they install |
| Data scope | Published products only | Products, orders, customers, costs, drafts |
| Rate limits | Standard storefront throttling | Documented leaky bucket |
The public endpoint returns exactly what a visitor browsing the shop can already see. It is the same data the theme uses to render collection pages, served in a machine-readable shape instead of HTML.
What the response looks like
You get an object with a single products array. Each product looks roughly like this, trimmed for readability:
{
"products": [
{
"id": 7234567890123,
"title": "Premium Cotton Tee",
"handle": "premium-cotton-tee",
"body_html": "<p>Heavyweight 240gsm cotton...</p>",
"published_at": "2026-03-11T09:14:22-05:00",
"updated_at": "2026-07-28T16:02:10-04:00",
"vendor": "Northbound",
"product_type": "T-Shirts",
"tags": ["cotton", "unisex"],
"variants": [
{
"id": 41234567890123,
"title": "Small / Black",
"option1": "Small",
"option2": "Black",
"sku": "NB-TEE-S-BLK",
"price": "24.99",
"compare_at_price": "34.99",
"available": true,
"grams": 180,
"requires_shipping": true
}
],
"images": [
{ "src": "https://cdn.shopify.com/s/files/.../tee-black.jpg", "position": 1 }
],
"options": [
{ "name": "Size", "values": ["Small", "Medium", "Large"] },
{ "name": "Colour", "values": ["Black", "White"] }
]
}
]
}
That is nearly everything you need to reconstruct a catalogue elsewhere: titles, descriptions with HTML intact, the full variant matrix with SKUs and prices, option definitions, and CDN image URLs.
Pagination, properly
The default page size is 30. Ask for more:
https://example.com/products.json?limit=250
https://example.com/products.json?limit=250&page=2
https://example.com/products.json?limit=250&page=3
250 is the ceiling and requesting more is silently clamped rather than rejected. Keep incrementing page until a response comes back with an empty products array. There is no total count in the payload, so an empty page is the only stop signal.
Do not hammer it. A 20,000 product store is 80 requests. Fired off in parallel with no delay, that looks like an attack and will earn you a throttle or a block. Sequential requests with a short pause between them finish in under a minute and upset nobody.
Some stores also accept a since_id parameter for cursor-style paging, which is more robust than page numbers when the catalogue is changing underneath you. Support is inconsistent, so treat it as a bonus rather than a plan.
What is deliberately missing
The gaps matter more than the contents if you are planning a migration.
| Missing | Why it matters | Where to get it |
|---|---|---|
| Meta title, meta description | SEO fields do not carry across a migration | Parse the product page head |
| Exact stock counts | You get available: true/false, not a number | Not publicly available |
| Metafields | Custom specs, ingredients, care instructions | Rendered page, if the theme prints them |
| Collection membership | Category structure is lost | /collections.json then each collection's products |
| Cost per item | Margin data, and rightly private | Admin API only |
| Unpublished and draft products | Not visible to the public at all | Admin API only |
Collections are recoverable with a second pass: /collections.json lists them, and /collections/{handle}/products.json gives the products in each. That is one extra request per collection, and it is the only way to rebuild category structure from public data.
When it returns nothing
An empty array or a 404 has three likely causes, and they call for different responses.
The store is not on Shopify
Check the response headers for a x-shopid or x-shardid header, or look for cdn.shopify.com in the page source. If neither is there, you are looking at WooCommerce, BigCommerce or something custom, each with its own equivalent.
The merchant has blocked it
Some stores intercept the path at the theme or edge level. It is not a supported Shopify setting, but it is achievable with Cloudflare rules or a redirect app. If the storefront works and the JSON path specifically does not, this is usually why.
Bot protection is intercepting the request
The most common case. A WAF sees a request with no browser fingerprint and serves a challenge page instead of JSON. The catalogue is still perfectly public, it just will not be handed to a bare HTTP client.
This is the situation that separates a weekend script from a production tool. Scrapify tries the fast JSON path first and, only when that returns nothing usable, falls back to reading the rendered storefront the way a browser does, extracting the same product data from JSON-LD and the DOM. Most stores never need the second phase, which is what keeps it quick.
Skip writing the scraper
Pagination, rate limiting, bot-protected stores and CSV formatting, handled. Paste a URL and download the catalogue.
Is reading it allowed?
The endpoint is public and unauthenticated, and returns only data the store already displays to shoppers. Under the leading US precedent, hiQ Labs v. LinkedIn, accessing publicly available data does not violate the Computer Fraud and Abuse Act.
That is not the whole picture. Product descriptions and photographs are copyrighted works, and republishing them verbatim is a copyright question entirely separate from the access question. Volume matters too: a request rate that degrades someone's storefront is a problem regardless of what the law says about access.
We go through the distinctions properly in is scraping product data legal.
What people actually use it for
- Migrations. Reading a catalogue out of Shopify to rebuild it in WooCommerce. See the Shopify to WooCommerce guide.
- Supplier catalogues. Pulling a dropship supplier's products into your own store, then keeping them synced.
- Price monitoring. Tracking competitor pricing over time, which is factual data and the least legally fraught use.
- Stock alerts. Watching the
availableflag on specific variants. - Auditing your own store. Finding products with no images, no SKU or a zero price faster than the admin UI allows.