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.jsonAdmin API
AuthenticationNoneAccess token required
Who can use itAnyoneThe store owner and apps they install
Data scopePublished products onlyProducts, orders, customers, costs, drafts
Rate limitsStandard storefront throttlingDocumented 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.

MissingWhy it mattersWhere to get it
Meta title, meta descriptionSEO fields do not carry across a migrationParse the product page head
Exact stock countsYou get available: true/false, not a numberNot publicly available
MetafieldsCustom specs, ingredients, care instructionsRendered page, if the theme prints them
Collection membershipCategory structure is lost/collections.json then each collection's products
Cost per itemMargin data, and rightly privateAdmin API only
Unpublished and draft productsNot visible to the public at allAdmin 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.

Try it on any store See pricing

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 available flag on specific variants.
  • Auditing your own store. Finding products with no images, no SKU or a zero price faster than the admin UI allows.

Frequently asked questions

Do I need an API key to read products.json?
No. This is the public storefront endpoint, not the authenticated Admin API. It returns whatever the store already shows to any visitor browsing the catalogue. The Admin API, which does need credentials, is a separate thing entirely and exposes orders, customers and cost data that products.json never includes.
How many products does products.json return at once?
Up to 250 per page, and 30 by default if you do not pass a limit. Request additional pages with ?limit=250&page=2 and keep going until a page comes back with an empty products array. A 5,000 product catalogue is 20 requests.
Why does products.json return an empty list or a 404?
Three common reasons: the store is not on Shopify at all, the merchant has disabled the endpoint through a theme or app level block, or a bot protection layer such as a WAF is intercepting non-browser requests. In the last case the catalogue is still readable, but only by something that renders the page like a real browser.
What is missing from products.json?
Meta title and meta description, inventory counts (you get an available boolean, not a number), cost of goods, metafields, and collection membership. If you need SEO fields you have to read the product page itself and parse the head tags or the JSON-LD block.

Keep reading

Guides Is Scraping Product Data Legal? What Store Owners Need to Know Shopify The Shopify Product CSV Template Explained, Column by Column Migration How to Migrate from Shopify to WooCommerce (Complete Guide)