How to integrate Shopify Storefront APIs with Webflow CMS to build scalable content-driven commerce sites

Learn how to sync Shopify Storefront API product data into Webflow CMS on Webflow Cloud to build scalable, content-driven commerce sites.

How to integrate Shopify Storefront APIs with Webflow CMS to build scalable content-driven commerce sites

Ismail Ajagbe
Technical Author
View author profile
Ismail Ajagbe
Technical Author
View author profile
Table of contents

Your product catalog lives in Shopify and your best sales content lives in Webflow, and the Storefront API finally lets them run from one source of truth instead of a spreadsheet someone updates by hand.

Shopify is very good at the parts of commerce that have to be correct: pricing, inventory, taxes, and checkout. Webflow excels at the parts that convince someone to buy: buying guides, lookbooks, campaign landing pages, and product stories that a marketing team can edit without filing a ticket.

The problem is that these two systems normally live apart, so product data gets copied by hand and drifts out of date the moment a price changes.

The Shopify Storefront API closes that gap. It is a GraphQL API that exposes products, collections, variants, and pricing to any front end you want to build.

You can pull that data into Webflow CMS so your content team works against live catalog data instead of a stale spreadsheet. When the sync runs on Webflow Cloud, the whole pipeline lives next to your site on the edge, with your API tokens kept server-side where they belong.

This guide builds one concrete end-to-end path: a scheduled server-side sync that reads products from the Shopify Storefront API and writes them into a Webflow CMS Collection as live, editable items.

What do you need to integrate the Shopify Storefront API with Webflow CMS?

You need a Shopify store with the Headless channel installed, a Storefront API access token, a Webflow site with a CMS Collection modeled for products, and a Webflow Cloud project to run the sync.

Both platforms have everything you need on their standard plans. Here is the full list to have in place before starting.

A Shopify store with the Headless channel

You need a Shopify store and the Headless sales channel installed from the Shopify App Store. The Headless channel is the standard way Shopify provisions Storefront API access for a custom storefront.

Once installed, it appears under Sales channels in your admin, and each storefront you create there generates the access tokens the API requires. You do not need a Shopify Plus plan for this; the Storefront API is available on standard Shopify plans.

The channel supports up to 100 active storefronts per shop, which is far more than a single integration needs.

If you are still using the legacy JS Buy SDK, note that Shopify deprecated it in early 2025 and now points every new build directly to the Storefront API, so starting here keeps you on the supported path.

A Storefront API access token with product scopes

The sync reads products and collections, so it needs a token carrying the unauthenticated_read_product_listings scope, which grants access to the Product and Collection objects.

The Headless channel issues two token types for each storefront:

  • A public access token sent in the X-Shopify-Storefront-Access-Token header
  • A private access token sent in the Shopify-Storefront-Private-Token header

The public token is designed to be exposed in a browser, while the private token must stay secret and server side. Because this sync runs entirely on the server, either works, and I use the public token stored as a server-side secret for its simplicity.

You configure the exact scopes per storefront, so grant only what the sync needs to read.

A Webflow CMS Collection modeled for products

You need a Webflow site with a CMS Collection whose fields mirror the Shopify product data you intend to store. At minimum, that means a name, a slug, a price, a description, a product image, and a plain text field to hold the Shopify product ID, which lets repeat syncs update the right item instead of creating a duplicate.

Modeling this Collection well is the difference between a sync that stays clean and one that fights you later, and the guide on structuring CMS Collections is worth reading before you commit to a schema.

Writing to the Collection requires an API token with the CMS:write scope.

A Webflow Cloud project to run the sync

You need a Webflow Cloud project running a Next.js app, either deployed or initialized locally with the Webflow CLI. Webflow Cloud runs your code on Cloudflare Workers at the edge through the OpenNext adapter, so the sync runs close to your data and never spins up a traditional server.

Node.js 22.13.0 or higher is required locally to run the CLI. One runtime detail matters for the code below: the Workers environment supports the Fetch API natively but does not provide Node modules like http or fs, so the sync uses plain fetch rather than any Node-dependent SDK. This same constraint shapes every Webflow Cloud integration.

7 steps to sync Shopify products into Webflow CMS

The build splits into three phases. First, you provision access on both platforms: a Storefront API token from Shopify and a site token plus Collection from Webflow. Then you write the read side, a Route Handler that queries the Storefront API over GraphQL and paginates the full catalog.

Finally, you write the write side, the transform and upsert logic that maps each Shopify product to a Webflow CMS item and publishes it live.

Let's go through each step.

1. Create a Storefront API token in Shopify

Install the Headless channel from the Shopify App Store if it isn't already installed. In your Shopify admin, go to Sales channels and click Headless, then click Add storefront to create one. Shopify generates a public and a private access token automatically for that storefront.

To set the scopes, select the Headless channel, choose your storefront from the list, and click Edit beside Storefront API permissions. Enable unauthenticated_read_product_listings so the token can read products and collections, then click Save.

Copy the public access token and the store domain (e.g., your-store.myshopify.com). Store both as environment variables in Step 3. The token is what authenticates every Storefront API request, and the domain is part of the endpoint URL.

Set the private token aside as well; you don't need it for this server-side read, but you'll need it if you add buyer-specific queries later.

2. Model your Webflow CMS Collection

In the Webflow Designer, create a CMS Collection named Products. Add the fields that mirror the Shopify data you plan to sync.

A workable starting schema is a Plain text field for the product title mapped to the built-in Name, the built-in Slug, a Number field for price, a Rich text or Plain text field for the description, an Image field for the featured image, and a Plain text field named Shopify ID.

That last field is the one people skip, and skipping it causes duplicated items on the second sync. The Shopify ID field stores the unique product identifier returned by the Storefront API, and the sync uses it to decide whether a product already exists in the CMS.

On every project, I make this field required and mark it as non-editable in the Designer so an editor cannot accidentally clear the value that keeps the two systems in step. Once the Collection is built, open Collection Settings and copy the Collection ID; you will need it in the next step.

3. Add your credentials as environment variables

The sync needs four values: your Shopify store domain, Storefront API token, Webflow site token, and Webflow Collection ID. Generate the Webflow site token from your site's Settings under Apps and integrations, then API access, and grant it the CMS:write scope so it can create and update items.

Add the four values to .env.local for local development and to your Webflow Cloud project's environment variables for production:

SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
SHOPIFY_STOREFRONT_TOKEN=your_public_storefront_access_token
WEBFLOW_SITE_TOKEN=your_webflow_site_token
WEBFLOW_COLLECTION_ID=your_products_collection_id

None of these need a NEXT_PUBLIC_ prefix, because every value is read on the server inside the Route Handler and never reaches the browser. In your Webflow Cloud dashboard, toggle Secret on SHOPIFY_STOREFRONT_TOKEN and WEBFLOW_SITE_TOKEN so they are encrypted at rest and masked in build logs.

Reading these values inside function bodies rather than at the top of a module is the safe pattern on Webflow Cloud, and the helpers below follow it.

Storing the tokens server-side is exactly the protection that a browser-based integration cannot offer, and building server-side API wrappers is a recurring reason teams choose Webflow Cloud in the first place.

4. Query the Shopify Storefront API from a Route Handler

Create a Route Handler that reads the catalog. The Storefront API is GraphQL-only, so every request is a POST to https://{store}.myshopify.com/api/{version}/graphql.json with a query in the body.

Pin the API version rather than using the latest alias, because Shopify releases a new version every quarter and supports each one for at least twelve months; pinning means a new release won't change your responses unless you decide to upgrade.

The helper below fetches products in pages of 50 and follows the cursor until it exhausts the catalog:

// lib/shopify.ts

const API_VERSION = '2026-07'

type ShopifyProduct = {
  id: string
  title: string
  handle: string
  description: string
  featuredImage: { url: string; altText: string | null } | null
  priceRange: { minVariantPrice: { amount: string; currencyCode: string } }
}

const PRODUCTS_QUERY = `
  query Products($cursor: String) {
    products(first: 50, after: $cursor) {
      pageInfo { hasNextPage endCursor }
      edges {
        node {
          id
          title
          handle
          description
          featuredImage { url altText }
          priceRange { minVariantPrice { amount currencyCode } }
        }
      }
    }
  }
`

export async function fetchAllProducts(): Promise<ShopifyProduct[]> {
  const domain = process.env.SHOPIFY_STORE_DOMAIN!
  const token = process.env.SHOPIFY_STOREFRONT_TOKEN!
  const endpoint = `https://${domain}/api/${API_VERSION}/graphql.json`

  const products: ShopifyProduct[] = []
  let cursor: string | null = null
  let hasNext = true

  while (hasNext) {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Storefront-Access-Token': token,
      },
      body: JSON.stringify({ query: PRODUCTS_QUERY, variables: { cursor } }),
    })

    if (!response.ok) {
      throw new Error(`Storefront API error ${response.status}`)
    }

    const { data } = await response.json() as {
      data: {
        products: {
          pageInfo: { hasNextPage: boolean; endCursor: string }
          edges: { node: ShopifyProduct }[]
        }
      }
    }

    products.push(...data.products.edges.map((edge) => edge.node))
    hasNext = data.products.pageInfo.hasNextPage
    cursor = data.products.pageInfo.endCursor
  }

  return products
}

The pagination loop matters for scale. The Storefront API caps each query at a page of results, so a catalog of any real size arrives across several requests, and the endCursor from one page becomes the after argument of the next.

Because the Storefront API scales with buyer traffic rather than enforcing a fixed request quota, this full-catalog read runs comfortably without special throttling. Reading the token inside the function keeps it out of module scope, which Webflow Cloud expects.

5. Map Shopify products to Webflow CMS fields

Shopify and Webflow describe a product differently, so you need a small transform that turns a Storefront product node into the fieldData shape the Webflow CMS API accepts.

This is also where you convert types, because Shopify returns prices as strings and a Webflow Number field expects a number:

// lib/mapProduct.ts
import type { ShopifyProduct } from './shopify'

export function toWebflowFieldData(product: ShopifyProduct) {
  return {
    name: product.title,
    slug: product.handle,
    price: parseFloat(product.priceRange.minVariantPrice.amount),
    description: product.description ?? '',
    'featured-image': product.featuredImage
      ? { url: product.featuredImage.url, alt: product.featuredImage.altText ?? product.title }
      : null,
    'shopify-id': product.id,
  }
}

The field keys on the left must match the slugs Webflow generated for your Collection fields, not their display names. Webflow lowercases a field name and replaces spaces with hyphens, so a field labeled Featured Image becomes featured-image and Shopify ID becomes shopify-id.

Reusing the Shopify handle as the Webflow slug is deliberate: it keeps your product URLs aligned with Shopify and gives you stable, human-readable paths for free. Storing the full Shopify ID in shopify-id is what the upsert in the next step keys on, so this mapping is the hinge the whole sync turns on.

6. Upsert products into Webflow CMS

Now write the products into the CMS. A durable sync never blindly creates items, because running it twice would double your catalog. Instead, it upserts: it reads existing items once, builds a map from Shopify ID to Webflow item ID, and then either updates an existing item or creates a new one.

This ID mapping is the same discipline that keeps any external source in sync, and it mirrors the approach used to sync data into CMS from other systems:

// app/api/sync/route.ts
import { NextResponse, type NextRequest } from 'next/server'
import { fetchAllProducts } from '@/lib/shopify'
import { toWebflowFieldData } from '@/lib/mapProduct'

const WF_BASE = 'https://api.webflow.com/v2'

async function loadExistingItems(collectionId: string, token: string) {
  const map = new Map<string, string>()
  let offset = 0

  while (true) {
    const res = await fetch(
      `${WF_BASE}/collections/${collectionId}/items?limit=100&offset=${offset}`,
      { headers: { Authorization: `Bearer ${token}` } }
    )
    const { items, pagination } = await res.json() as {
      items: { id: string; fieldData: Record<string, unknown> }[]
      pagination: { total: number }
    }

    for (const item of items) {
      const sid = item.fieldData['shopify-id'] as string | undefined
      if (sid) map.set(sid, item.id)
    }

    offset += items.length
    if (offset >= pagination.total || items.length === 0) break
  }

  return map
}

export async function POST(request: NextRequest) {
  if (request.headers.get('authorization') !== `Bearer ${process.env.SYNC_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const collectionId = process.env.WEBFLOW_COLLECTION_ID!
  const token = process.env.WEBFLOW_SITE_TOKEN!

  const products = await fetchAllProducts()
  const existing = await loadExistingItems(collectionId, token)

  let created = 0
  let updated = 0

  for (const product of products) {
    const fieldData = toWebflowFieldData(product)
    const itemId = existing.get(product.id)

    if (itemId) {
      await fetch(`${WF_BASE}/collections/${collectionId}/items/live`, {
        method: 'PATCH',
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ items: [{ id: itemId, fieldData }] }),
      })
      updated++
    } else {
      await fetch(`${WF_BASE}/collections/${collectionId}/items/live`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ fieldData }),
      })
      created++
    }
  }

  return NextResponse.json({ created, updated })
}

Two details make this production safe. First, the handler writes to the /items/live endpoints, which create and publish in one call so products appear on the live site without a separate publish step.

Second, a SYNC_SECRET bearer check protects the sync, so only your scheduler can trigger a full catalog write. Add SYNC_SECRET to your environment variables alongside the other four. The created and updated counts in the response give you a quick signal that the sync did what you expected on each run.

7. Automate the sync on a schedule

Triggering the sync by hand is fine while you build, but a content-driven store needs the CMS to track Shopify without anyone having to remember to press a button. Two triggers cover almost every case.

For scheduled freshness, point a scheduler at your sync endpoint so it sends the same authenticated POST every hour, which keeps prices and inventory current within a predictable window.

For near real-time accuracy, register a Shopify webhook on the products/update and products/create topics that points at a lighter version of this handler, so a single changed product syncs the moment it changes in Shopify.

You can start a manual run to confirm the whole pipeline works before wiring any automation:

curl -X POST https://your-app.webflow.io/api/sync \
  -H "Authorization: Bearer your_sync_secret"

The response reports how many items were created and updated. Open your Webflow CMS Collection and confirm the products appear with prices, images, and the Shopify ID populated on each.

Once the manual run is clean, the scheduled trigger runs the same code on its own cadence. For most content-driven stores, I set an hourly schedule for the full catalog and layer a product-update webhook on top for the handful of SKUs that change often, which balances freshness against the number of writes.

What breaks a Shopify to Webflow CMS sync?

Most sync failures trace to a few predictable causes: a Storefront token missing the product scope, field keys that don't match the Collection slugs, duplicate slugs colliding on write, hitting the Webflow API rate limit during a large sync, or a Node-dependent SDK that will not run on the edge.

Each one fails in a recognizable way, which makes them quick to diagnose once you know the signature.

Here is how to recognize each and what to fix.

The Storefront API returns an empty product list

If your GraphQL query succeeds but products.edges comes back empty, the token almost always lacks the right scope. The Storefront API doesn't error on a scope it cannot serve; it simply returns nothing for objects the token cannot read.

Open the Headless channel in your Shopify admin, select the storefront, click Edit beside Storefront API permissions, and confirm unauthenticated_read_product_listings is enabled. Save and rerun the query.

A second common cause is querying a store with no published products, since the Storefront API exposes only products published to the Headless channel. Confirm the products are active and available on that channel before assuming the token is at fault.

Webflow rejects the write with a field mismatch

A 400 response from the Webflow CMS API on create or update usually means a key in your fieldData does not match a field slug in the Collection. The API validates against the exact slugs Webflow generated, not the display names you see in the Designer.

Open Collection Settings, click into each field, and read the field slug rather than assuming it from the label.

A field shown as Featured Image is almost always featured-image, and a custom field can pick up a numeric suffix like price-2 if you created and deleted a similarly named field earlier. Matching the mapping in Step 5 to the real slugs resolves this immediately.

Duplicate slugs collide on the second sync

Webflow requires every item slug in a Collection to be unique, so if two Shopify products share a handle, or if a manual item already claimed a slug, the write fails with a conflict. The upsert in Step 6 prevents the most common version of this by updating existing items rather than recreating them, but a genuine handle collision in Shopify still needs handling.

My fix is to fall back to appending the last segment of the Shopify ID to the slug when a conflict is detected, which guarantees uniqueness without losing the readable handle for products that don't collide.

Keeping the Shopify ID as the true key and treating the slug as a display convenience avoids this whole class of problems.

The sync hits the Webflow API rate limit

A large catalog written item by item can exceed the Webflow Data API rate limit: 60 requests per minute on Starter and Basic sites, and 120 requests per minute on CMS, Business, and Ecommerce plans.

When you cross it, the API returns a 429 with a Retry-After header. The fix is to respect that header and pace the writes rather than firing them as fast as the loop allows.

For catalogs in the low thousands, batching writes and pausing between batches keeps you under the ceiling, and reading X-RateLimit-Remaining from each response lets you slow down before you hit zero. On a large one-time import I run the sync in chunks rather than a single pass.

A Node-only SDK fails to run on the edge

If your build errors with a message about a missing http, https, or fs module, something in your dependency tree expects Node.js APIs that the Cloudflare Workers runtime does not provide. The legacy shopify-buy SDK is the usual culprit, since it predates the edge and pulls in Node internals.

The fix is the approach this guide already uses: call the Storefront API with plain fetch and a GraphQL string, which needs no SDK at all and runs natively on the Workers runtime. The same rule applies to the Webflow side, where a direct fetch against the Data API is both lighter and more reliable on the edge than any Node-dependent wrapper.

Give your product content a home your team can run

From here, the natural next move is to decide how far into commerce you want Webflow to reach. If you only need product content and route buyers back to Shopify for the purchase, this read-only sync is the whole job.

If you want to handle payment inside your own experience, adding Stripe Checkout in Webflow is a common next layer, along with larger builds that combine auth, database, and payments, which show how several services sit together in one Webflow Cloud project.

For a broader view of where this pattern fits, our headless commerce guide frames the tradeoffs against a traditional store.

Frequently asked questions

Should I use the public or private Storefront API token?

For a server-side sync, either works, because the token never reaches the browser. The public token is simpler and is what the Headless channel issues by default. Use the private token only when you need authenticated buyer context, and always keep it server-side.

How often should the sync run?

It depends on how fast your data changes. An hourly scheduled sync keeps prices and inventory current for most stores. If you need same-second accuracy for specific products, add a Shopify products/update webhook so individual changes sync the moment they happen in Shopify.

Can visitors check out through this integration?

No. This sync is read-only and brings product content into Webflow CMS for display and editing. Checkout still happens through Shopify or a separate payment layer. You can link buyers to Shopify's hosted checkout, or handle payment yourself with a tool like Stripe.

Do I need a Shopify Plus plan for the Storefront API?

No. The Storefront API is available on standard Shopify plans through the Headless sales channel. You install the channel, create a storefront, and grant the product scope. Shopify Plus adds capabilities elsewhere, but you don't need it to read product data this way.

Which Storefront API version should I pin?

Pin a dated version like 2026-07 rather than the latest alias. Shopify ships a new version each quarter and supports each for at least twelve months. Pinning means a new release never changes your responses unexpectedly, and you upgrade on your own schedule after testing.

Why store the Shopify product ID in the CMS?

The Shopify ID is the stable key that lets a repeat sync recognize a product it already imported. Without it, the sync cannot tell an existing product from a new one, so every run creates duplicates. Storing it turns a fragile import into a reliable upsert.


Last Updated
September 4, 2026
Category

Related articles


verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo

Get started for free

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Watch demo

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.