How to Integrate Saleor with Your Headless CMS
Sync product stories, SEO fields, badges, and campaign metadata from your headless CMS into Saleor the moment editors publish.
What is Saleor?
Saleor is an open-source, API-first commerce platform built around a GraphQL API, a dashboard, and a modular app system. Teams use it to run product catalogs, checkout, orders, discounts, channels, payments, and fulfillment across custom storefronts. It’s a common fit for commerce teams that need more control than a hosted storefront gives them, especially for marketplaces, multi-region catalogs, and custom buying flows.
Why integrate Saleor with a headless CMS?
Saleor handles the commerce system of record: products, variants, prices, inventory, checkout, orders, discounts, and customer accounts. But commerce teams usually need richer content around those records, like buying guides, PDP storytelling, SEO copy, comparison tables, ingredient callouts, regional promo modules, and merchandising pages. If that content lives outside your commerce workflow, editors end up copying text between tools, developers hardcode campaign sections, and storefronts ship with stale product copy.
Connecting Saleor to a headless CMS fixes that split. Saleor stays responsible for commerce logic, while structured content provides the editorial layer that makes products easier to sell. With Sanity, that content is typed JSON in the Content Lake, so a product description, care guide, badge list, or locale-specific SEO field can be queried directly with GROQ. No HTML scraping. No blob parsing. Webhooks can fire on publish, and Functions can run the sync logic server-side, so Saleor gets the fields it needs as soon as the editorial team publishes.
The trade-off is that you need to decide what owns each field. Saleor should usually own price, tax, stock, checkout, and order state. Sanity should usually own editorial content, merchandising modules, localized copy, and channel-specific product storytelling. Draw that line early, or you’ll create two sources for the same product data.
Architecture overview
A typical Saleor and Sanity integration starts with product-adjacent content modeled in Sanity Studio. For example, a `productMarketing` document can include `saleorProductId`, `seoTitle`, `seoDescription`, `badges`, `ingredients`, `careInstructions`, and references to campaign modules. When an editor publishes that document, a Sanity webhook can send the document ID to a webhook endpoint, or a Sanity Function can run directly on the content mutation. The sync layer then uses `@sanity/client` to fetch the published document from the Content Lake with GROQ. That query should project only the fields Saleor needs, including joined reference data such as badges, category landing-page copy, or localized snippets. The sync layer calls Saleor’s GraphQL API with an app token, commonly using mutations such as `productUpdate` for product fields and `updateMetadata` for extra structured values that don’t belong in Saleor’s core product schema. From there, the storefront can work in one of two ways. It can query Saleor for commerce data and Sanity for editorial content at render time, which keeps each system clean. Or it can read selected synced fields directly from Saleor, which can be useful when another Saleor-connected channel, like a marketplace connector or internal tool, needs those values. For most storefronts, the first option gives you clearer ownership and fewer sync edge cases.
Common use cases
Editorial product pages
Pair Saleor product, variant, price, and availability data with Sanity buying guides, badges, comparison blocks, and localized PDP copy.
Multi-channel merchandising
Use Sanity to define channel-specific product messaging, then sync approved fields to Saleor channels for web, mobile, and regional storefronts.
SEO and launch updates
Publish SEO titles, meta descriptions, campaign labels, and structured product metadata in Sanity, then update the matching Saleor product through GraphQL.
Shopping agent context
Let AI agents answer product questions using Sanity’s structured editorial content and Saleor’s live commerce data for price, stock, and checkout state.
Step-by-step integration
- 1
Set up Saleor access
Create a Saleor Cloud project or use a self-hosted Saleor instance. In the Saleor Dashboard, create an app or integration token with product permissions, such as `MANAGE_PRODUCTS`, and copy your GraphQL API URL, usually ending in `/graphql/`.
- 2
Install the integration packages
For a server-side sync endpoint, install `@sanity/client` and a GraphQL client such as `graphql-request` to call Saleor’s GraphQL API. If you’re building a Saleor app UI, add `@saleor/app-sdk` as well.
- 3
Model Saleor-linked content in Sanity Studio
Create a schema such as `productMarketing` with fields like `saleorProductId`, `title`, `slug`, `seoTitle`, `seoDescription`, `badges`, `locale`, and references to campaign modules. Store the Saleor product GraphQL ID, for example `UHJvZHVjdDo3Mg==`, so the sync knows exactly which product to update.
- 4
Create the sync trigger
Add a Sanity webhook filtered to published `productMarketing` documents, or use a Sanity Function triggered by create and update mutations. Send a small payload, such as `{ "_id": _id }`, then fetch the full document from the Content Lake with GROQ inside the handler.
- 5
Call Saleor’s GraphQL API
Use Saleor mutations such as `productUpdate` for fields like name, slug, SEO title, and SEO description. Use `updateMetadata` for structured editorial values, such as badges, Sanity document IDs, or campaign labels that Saleor doesn’t model as product fields.
- 6
Test the storefront path
Use Saleor’s GraphQL Playground or API explorer to confirm the product updated. Use Sanity Vision to test the GROQ query. Then build the frontend, for example with Next.js, so it reads Saleor for commerce data and Sanity for rich editorial sections.
Code example
A minimal Next.js route that receives a Sanity webhook, fetches the published product content with GROQ, and updates the matching Saleor product through GraphQL.
import { createClient } from "@sanity/client";
import { GraphQLClient, gql } from "graphql-request";
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: "2025-01-01",
token: process.env.SANITY_READ_TOKEN,
useCdn: false
});
const saleor = new GraphQLClient(process.env.SALEOR_API_URL!, {
headers: { Authorization: `Bearer ${process.env.SALEOR_APP_TOKEN}` }
});
const query = `*[_id == $id][0]{
_id, title, "slug": slug.current, seoTitle, seoDescription,
saleorProductId, badges[]
}`;
const PRODUCT_UPDATE = gql`
mutation ProductUpdate($id: ID!, $input: ProductInput!) {
productUpdate(id: $id, input: $input) {
product { id slug }
errors { field message }
}
}
`;
const UPDATE_METADATA = gql`
mutation UpdateMetadata($id: ID!, $input: [MetadataInput!]!) {
updateMetadata(id: $id, input: $input) {
errors { field message }
}
}
`;
export async function POST(req: Request) {
const { _id } = await req.json();
const doc = await sanity.fetch(query, { id: _id });
if (!doc?.saleorProductId) {
return Response.json({ error: "Missing Saleor product ID" }, { status: 400 });
}
const updated = await saleor.request(PRODUCT_UPDATE, {
id: doc.saleorProductId,
input: {
name: doc.title,
slug: doc.slug,
seoTitle: doc.seoTitle,
seoDescription: doc.seoDescription
}
});
await saleor.request(UPDATE_METADATA, {
id: doc.saleorProductId,
input: [
{ key: "sanityDocumentId", value: doc._id },
{ key: "badges", value: JSON.stringify(doc.badges || []) }
]
});
return Response.json({ ok: true, updated });
}How Sanity + Saleor works
Build your Saleor integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs you need to connect editorial product content with Saleor commerce data.
Start building free →CMS approaches to Saleor
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Product content modeling | Product content often lives in page templates or plugins, which makes reuse across Saleor channels harder. | Schema-as-code lets you model product stories, badges, guides, regions, and Saleor product IDs as typed, version-controlled content. |
| Real-time sync to Saleor | Updates often depend on scheduled exports, plugin behavior, or manual copy-and-paste into Saleor. | Webhooks and Functions can react to publish events, fetch the full document with GROQ, and call Saleor’s GraphQL API without polling. |
| Field-level query control | APIs may return page-shaped content, which can include fields Saleor doesn’t need. | GROQ can project the exact Saleor sync payload, including referenced badges, localized copy, and campaign modules. |
| Commerce ownership boundaries | Commerce and editorial fields can get mixed together in plugins, making ownership unclear. | Sanity works well as the editorial source while Saleor remains the source for price, inventory, checkout, orders, and tax logic. |
| Multi-channel delivery | Content is often shaped for a website first, then adapted for mobile, email, or marketplace use later. | The same structured content can feed web, mobile, Saleor sync jobs, support tools, and production AI agents through Agent Context. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Stripe
Connect structured product, plan, or campaign content with Stripe pricing, subscriptions, invoices, and checkout flows.
Sanity + BigCommerce
Pair BigCommerce catalog and checkout data with Sanity-powered landing pages, buying guides, and localized product storytelling.
Sanity + Medusa
Use Sanity for editorial commerce content while Medusa handles custom carts, orders, regions, and fulfillment logic.