The Storefront API is Shopify's buyer-facing, read-only GraphQL surface for building headless and custom front ends. You query products and collections with a Storefront access token and get exactly what a shopper is allowed to see — published products, variant options, prices, and availability — resolved per market with the @inContext directive. This guide is catalog-read recipes only: the queries, the fields, pagination, and localized reads.
This is the read side of the Shopify Product Catalog API guide. For the full Storefront surface — cart, checkout, customer auth, and token setup — see the Shopify Storefront API guide; this piece deliberately does not restate that. Everything below uses Storefront GraphQL API version 2026-01 (Shopify: Storefront API reference).
Admin-read vs Storefront-read: which context you're in
The Admin API and Storefront API both read catalog data, but they answer different questions. The Admin API is the merchant's source of truth; the Storefront API is the customer's view. Choosing wrong is the most common headless catalog mistake.
| Concern | Admin API (read) | Storefront API (read) |
|---|---|---|
| Audience | Back office, internal jobs | Buyers, browser, mobile app |
| Token | Admin access token (server-side) | Storefront token (public or private) |
| Visibility | Every product, any status | Only published, ACTIVE products |
| Pricing | Base/shop prices | Market-resolved via @inContext |
| Throttling | Calculated query cost | Request-rate based |
| Endpoint | /admin/api/2026-01/graphql.json | /api/2026-01/graphql.json |
If you need drafts, unpublished items, or full inventory truth, that's an Admin read — see Admin API vs Storefront API for catalog data. If you're rendering a storefront a shopper actually browses, stay on the Storefront API.
Tokens: public vs private
Two token types authenticate Storefront requests. A public access token is meant to be exposed in client-side code — it grants only unauthenticated, buyer-facing scopes and is what you ship in a browser or mobile catalog browser. A private (delegate) access token stays server-side and can carry elevated unauthenticated scopes. For catalog reads, a public token is almost always the right call (Shopify: Storefront authentication).
Every request sends the token in the Shopify-Storefront-Access-Token header:
async function storefront(shop, token, query, variables = {}) {
const res = await fetch(`https://${shop}/api/2026-01/graphql.json`, {
method: "POST",
headers: {
"Shopify-Storefront-Access-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
return res.json();
}
Recipe 1: paginate the product catalog
The products connection is your entry point for a headless catalog. Request only the fields the buyer UI renders — the Storefront API filters to published, ACTIVE products automatically, so you never leak drafts.
query CatalogPage($cursor: String) {
products(first: 24, after: $cursor, sortKey: BEST_SELLING) {
pageInfo { hasNextPage endCursor }
nodes {
id
handle
title
productType
vendor
availableForSale
featuredImage { url altText }
priceRange {
minVariantPrice { amount currencyCode }
maxVariantPrice { amount currencyCode }
}
}
}
}
Loop until hasNextPage is false, passing endCursor back as $cursor. The Storefront API is throttled by request rate, not the Admin API's cost buckets, so pace the loop and lean on caching — catalog reads are highly cacheable at the CDN edge (Shopify: Storefront rate limits).
A few field choices matter for a buyer-facing list. priceRange gives you a "from $X" label without fetching every variant, which keeps the query cheap on a grid of 24+ cards. availableForSale at the product level lets you badge sold-out items in the grid before anyone opens the detail page. And handle — not the global id — is what you route on, because it maps to the human-readable URL a shopper and Google both expect. Resist the urge to over-fetch variants in a list query; save the full variant matrix for Recipe 2, when the buyer actually lands on a product.
async function fetchStorefrontCatalog(shop, token) {
const products = [];
let cursor = null;
do {
const { data } = await storefront(shop, token, CATALOG_PAGE, { cursor });
products.push(...data.products.nodes);
cursor = data.products.pageInfo.hasNextPage
? data.products.pageInfo.endCursor
: null;
} while (cursor);
return products;
}
Recipe 2: read one product with variants and options
A product detail page needs the full option matrix — every variant, its selectedOptions, price, compare-at price, and buyable state. Fetch by handle, which is the stable, URL-friendly identifier the Storefront API is built around.
query ProductByHandle($handle: String!) {
product(handle: $handle) {
id
title
descriptionHtml
options { name values }
variants(first: 100) {
nodes {
id
title
sku
availableForSale
quantityAvailable
selectedOptions { name value }
price { amount currencyCode }
compareAtPrice { amount currencyCode }
}
}
}
}
options { name values } gives you the swatches and dropdowns (Size, Color); selectedOptions on each variant tells you which combination that variant represents, so the UI can map a buyer's selection to a specific variant id for the cart. availableForSale and quantityAvailable drive the "Add to cart" vs "Sold out" state without exposing the merchant's full inventory (Shopify: ProductVariant).
Recipe 3: read a collection
Collections power category and landing pages. Query a collection by handle and page its nested products connection exactly like the top-level one.
query CollectionPage($handle: String!, $cursor: String) {
collection(handle: $handle) {
title
descriptionHtml
products(first: 24, after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes {
id
handle
title
featuredImage { url altText }
priceRange { minVariantPrice { amount currencyCode } }
}
}
}
}
Use the top-level collections(first:) connection to build navigation menus, and the per-collection products connection to render each category page. Both honor the same published-only visibility rules.
Two practical notes. First, a collection's products connection accepts the same first/after cursor pattern, so a large category paginates identically to the top-level catalog — reuse the same loop. Second, prefer querying by handle over a global id for menus and category routes: handles survive across API versions and match your URL structure, whereas relying on id couples your front end to internal identifiers. For faceted filtering inside a collection, layer the filters argument on the connection rather than pulling everything and filtering client-side, which wastes both bandwidth and cache.
Recipe 4: market and language reads with @inContext
For a store using Shopify Markets, prices, availability, and translated content differ per country and language. The @inContext directive resolves the whole query in one buyer context — no separate storefront per market.
query LocalizedCatalog($cursor: String) @inContext(country: CA, language: FR) {
products(first: 24, after: $cursor) {
nodes {
title
priceRange {
minVariantPrice { amount currencyCode }
}
}
}
}
With country: CA, language: FR, prices come back in the Canadian market's currency and titles/descriptions in French where translations exist. Omit @inContext and you get the shop's default context, which is a subtle bug in a multi-market storefront — buyers see the wrong currency. Always set it explicitly and pass the buyer's resolved country and language (Shopify: @inContext directive).
What the Storefront catalog will not give you
Because it's the buyer's view, the Storefront API omits everything a shopper shouldn't see: draft and archived products, unpublished variants, cost/margin data, and full inventory counts across locations (quantityAvailable is a single buyable number, not per-location stock). It's also read-only — you cannot create or edit catalog data through it. When you need the source of truth, or you're building a feed for ads and AI shopping, that's an Admin-side read; the product catalog API guide covers that write/export path.
From catalog reads to a real front end
These four queries are the backbone of a headless storefront: paginate products, resolve a product detail page, render a collection, and localize with @inContext. The next step is wiring them into an actual browsing UI — filtering, faceted search, and variant selection — which is exactly what the headless Shopify catalog browser walkthrough builds.
Where this meets revenue: a fast, correctly-localized catalog is only half the equation. The other half is the traffic and the data quality feeding your ad and AI shopping channels — the work AdsX does for Shopify brands so a headless build actually converts.
Next steps
- Full Storefront surface (cart, checkout, auth): the Shopify Storefront API guide.
- Picking a read path: Admin API vs Storefront API for catalog data.
- Build the UI on top of these reads: headless Shopify catalog browser.
- Surface overview: the Shopify Product Catalog API guide.