Product data from ERP and PIM, content from Sanity: how to connect the systems

Product data belongs in the ERP or PIM, editorial content in Sanity: every data type gets exactly one leading system. Three patterns cover the connection: webhook push via the mutation API, pull at build or request time, and references to external IDs. Prices stay out, and field ownership plus a status instead of deletion spare you the typical sync accidents.
9 min readMatthias RadscheitMatthias Radscheit
Happycodingen-US

TL;DR

Product data belongs in the ERP or PIM, editorial content in Sanity: every data type gets exactly one leading system. Three patterns cover the connection: webhook push via the mutation API, pull at build or request time, and references to external IDs. Prices stay out, and field ownership plus a status instead of deletion spare you the typical sync accidents.

  • One single source of truth per data type: the ERP owns prices and stock, the PIM owns attributes and variants, Sanity owns the editorial content. No field has two owners.
  • Webhook push into the Content Lake works with deterministic document IDs derived from the SKU and the createOrReplace mutation; Akeneo's old Events API is only supported until December 31, 2026 (as of October 2026).
  • Sanity's webhooks deliver at-least-once with an idempotency key and two retries 30 seconds apart: your receiver has to handle duplicates.
  • Prices and stock never belong in the Content Lake: the frontend fetches them from the ERP at request time, otherwise the website will eventually sell at an old price.
  • A PIM only pays off once you have several output channels or genuine attribute maintenance; below roughly 500 products with one website as the only channel, Sanity alone is enough.

Two systems, two truths: why the separation is right

I see the same pattern in almost every conversation with manufacturers and retailers: the product data lives in the ERP or PIM, say SAP Business One or Akeneo. Marketing wants to build landing pages, guides, and campaigns. And someone suggests "just copying everything into the CMS". That suggestion is where most projects later fail: the copy goes stale, maintenance doubles, and in the end nobody knows which price is correct.

A quick note on who is writing here: I'm Matthias, a one-person agency called happycoding.agency in Berlin. I build websites and portals on Sanity and connect them to the systems my clients already run. What Sanity fundamentally is and how the Content Lake works is something I cover in a separate introductory article.

In this article, you get the architecture that has proven itself in my projects: clear data ownership per data type, three sync patterns between ERP, PIM, and Sanity, the typical pitfalls from day-to-day operations, and a decision guide for when you need a PIM at all.

Single source of truth: every data type has exactly one leading system

First, the core principle: for every data type, there is exactly one source of truth. All other systems hold copies at most, and every copy knows it is a copy. As soon as two systems are allowed to own the same field, you no longer have a dataset — you have two opinions.

This is how I split responsibilities in projects with manufacturers and retailers:

Data typeLeading systemExamples
Master data, prices, stockERParticle numbers, terms, inventory levels
Attributes, variants, translationsPIMtechnical data, classification via ETIM or ECLASS
Editorial contentSanitylanding pages, guides, brand storytelling, SEO copy
TransactionsShop or ERPcart, order, invoice

This holds across industries. At a car dealership, the vehicle data comes from the dealer management system and flows to the listing platforms via feed; the guide page "Why buy a one-year-old car?" belongs in the CMS. At a technical wholesaler, the PIM maintains 40,000 articles with ETIM attributes; the application reports and selection guides are created in Sanity.

From this follows the most important rule of this article: only what editors are responsible for belongs in the CMS. Three things have no place in Sanity, not even "just to be safe" as a copy:

  • Prices: They change according to ERP logic, sometimes per customer. When in doubt, a copy in the CMS is wrong.
  • Stock: It goes stale by the minute. No sync interval is short enough when the website promises availability.
  • Attribute logic: Inheritance, variants, and classification are core tasks of the PIM. Rebuilt in the CMS, they become a second, worse implementation.

The reverse check works the same way: what is maintained in the CMS must not be overwritten by any other system. A product's marketing description can therefore deliberately live in Sanity while the technical description comes from the PIM. Two fields, two owners, no conflict.

Three sync patterns between ERP, PIM, and the Content Lake

Now to the core question: how do the systems come together without data being maintained twice? Three patterns cover almost all cases in my practice. Which one fits depends on how fresh the data on the website needs to be and whether editors should work with it.

Pattern 1: webhook push into the Content Lake

The PIM reports changes, a small service writes them to Sanity. Akeneo ships the Event Platform for exactly this: you subscribe to product events instead of polling the API on a schedule. The older Events API is deprecated and only supported until December 31, 2026 (as of October 2026).

On the Sanity side, the mutation API receives the data. The decisive trick is a deterministic document ID, such as product-{SKU}: with the createOrReplace mutation, every sync updates the same document instead of creating duplicates. A transaction bundles several mutations, so a product update arrives completely or not at all.

For bulk imports, the limits are worth a look: a query-based mutation touches at most 10,000 documents; for larger catalogs, you paginate by _id. For the nightly full sync of 40,000 articles, that means several transactions, ideally with your own transactionId, so you can trace which batch went through if something fails.

You choose this pattern when editors should work with product data in Sanity: they see titles and attributes in the Studio, reference products in landing pages, and the synced fields are read-only because the PIM owns them.

Pattern 2: pull at build or request time

The opposite direction: Sanity stores no product data at all. Your frontend queries both APIs and merges the responses into the page, either at static build time or live per request. I would handle prices and stock this way without exception: live from the ERP endpoint, never from a copy.

With the pull approach, caching deserves a look: Sanity's API CDN answers GROQ queries from cache and takes load off both build and runtime. For editorial content, that is ideal. The price from the ERP, however, you deliberately fetch past the cache — otherwise staleness sneaks back in through the back door.

The cost of this pattern: editors do not see the products in the Studio. For a catalog with stock levels changing hourly, that does not matter; for curated landing pages, it is a real loss. That is why I usually combine pattern 2 with the third one.

Pattern 3: references to external IDs instead of data copies

The Sanity document stores only the key, say the SKU, and the frontend resolves it against the ERP or PIM at runtime. In practice, a middle ground has proven itself: a lean product stub in Sanity with ID, name, and image, while everything else stays out. In the schema, the stub is its own document type with read-only fields and the SKU as a required field.

Editors reference the stub like any other document, and the truth stays in the source system. You get both: curatable product placements in the Studio and current data on the website, without copying the full set of attributes.

The opposite direction is covered too: Sanity's GROQ-powered webhooks fire on create, update, and delete, with a GROQ filter and a payload you define freely. Delivery is at-least-once with an idempotency key, two retries 30 seconds apart, and a timeout after 30 seconds (as of October 2026). Secure the receiver with the webhook secret; Sanity signs requests using the same scheme as Stripe.

If you do not want to run your own server for the sync logic: Sanity Functions execute your code directly on Sanity's infrastructure, triggered by document events and filtered via GROQ. A function runs up to 10 seconds by default, configurable up to 900 seconds (as of October 2026). That is enough for lean sync tasks; a full-blown nightly import I still run as a separate job.

By the way, the principle is not exclusive to Sanity: I answered the same integration question for the WordPress world in the article Connecting WordPress to CRM, ERP, and PIM. The difference lies in the toolbox, not in the architecture.

Three pitfalls from my project work

The patterns are quickly explained; the mistakes hide in day-to-day operations. The following three points come from my happycoding project work; I describe them as scenarios without client names, because the patterns repeat across industries.

Sync conflicts: two writers, one field

The scenario: the sync writes the product description from the PIM, an editor improves it in the Studio in the evening, and the nightly sync silently overwrites her work. Nobody notices until someone complains.

The solution is field ownership instead of hope: every field in the schema belongs either to the machine or to the human, never to both. You mark machine fields as readOnly in the Studio, and the sync never touches editorial fields. Technically, that means targeted patch mutations on the machine fields instead of createOrReplace on the whole document.

Delete propagation: the product is gone, the reference is not

The scenario: an article is dropped from the range, the PIM deletes it, but five landing pages in Sanity still reference it. Sanity refuses to delete a document that strong references point to; your sync service aborts with an error, and from then on it processes nothing at all.

My approach: do not delete product stubs — set them to a status like "discontinued" instead. The frontend hides them, editors see in the Studio why a page has a hole, and they clean up the references at their own pace. And plan your own webhook configuration deliberately: unpublishing a document also fires the delete trigger.

Price freshness: the most expensive copy

The scenario: prices were synced into the Content Lake "to keep things simple". Then the ERP changes the terms, the sync hangs for two hours, and the website sells at the old price. In B2B with customer-specific terms, that is more than embarrassing — it puts trust at risk.

That is why I repeat the rule from the first section as a maxim: a price in the CMS is not a price, it is a rumor. The frontend fetches prices and stock from the ERP endpoint at request time; where load does not allow that, a short-lived cache of a few minutes with a visible timestamp helps.

Decision guide: when PIM plus Sanity, when Sanity alone?

That leaves the question of whether you need the dual architecture at all. In my project conversations, three signals speak for a PIM: more than one output channel, a team that maintains attributes as a distinct task, and trading partners that demand classifications like ETIM. If all three are missing, skip the system.

SituationRecommendation
Below roughly 500 products, one website as the only channelSanity alone, products as their own document type
Product data goes to website, shop, marketplaces, and printPIM leads, Sanity adds the content (pattern 1 or 3)
ERP in place, but no dedicated attribute maintenanceconnect ERP and Sanity directly, without a PIM in between
Shop with its own product management, such as MedusaJSthe shop owns the product data, Sanity delivers the storytelling
Prices and stock on the websitealways live from ERP or shop, never from the CMS

Many underestimate the first row: Sanity can hold product data fully, with validation, variants as objects, and GROQ as the query language. As long as only the website needs the data and a small team maintains it, a PIM would be extra infrastructure without a job to do.

The fourth row is one I have worked through in detail for shop projects, in my guide on combining MedusaJS with a headless CMS: the same architecture question from the e-commerce perspective, including the question of who renders the product page.

Next steps

If you are facing this architecture decision right now, answer three questions: which data type has no clearly leading system today? How fresh do prices on the website need to be? And who should maintain product content going forward? With these answers, the basic structure almost builds itself. How I plan and deliver Sanity projects is on my Sanity services page.

You would rather not sort out the answers alone? Send me your system landscape, and I will sketch out in a call which pattern fits it and where the pitfalls are: Book a no-obligation intro call.

Frequently asked questions

Can Sanity replace a PIM?
For small catalogs, yes: below roughly 500 products with the website as the only channel, you can maintain products as their own document type in Sanity, including validation and variants. As soon as several output channels, inheritance logic, or classifications like ETIM come into play, a PIM is the right tool, and Sanity stays responsible for the editorial content.
How does product data get from Akeneo into Sanity?
Via the Akeneo Event Platform you subscribe to product events, and a small service or a Sanity Function writes the changes into the Content Lake via the mutation API. The trick: deterministic document IDs derived from the SKU and the createOrReplace mutation, so every sync updates the same document. The older Events API is deprecated and only runs until December 31, 2026 (as of October 2026).
Do prices and stock levels belong in the CMS?
No. Both change according to ERP logic and go stale in every copy. The frontend fetches them from the ERP or shop endpoint at request time; where load does not allow that, a short-lived cache of a few minutes with a visible timestamp helps. Only what editors are responsible for belongs in the CMS.
What happens when a product is deleted in the PIM?
Do not delete the product stub in Sanity right away: Sanity refuses to delete a document with strong references, and your sync aborts. Instead, set a status like "discontinued", have the frontend hide such products, and clean up the references editorially. Also note: unpublishing fires the delete trigger on webhooks too.
Do I need my own server for the synchronization?
Not necessarily. Sanity Functions run sync logic directly on Sanity's infrastructure, triggered by document events and filtered via GROQ. A function runs up to 10 seconds by default, configurable up to 900 seconds (as of October 2026). That is enough for lean tasks; a nightly full import of large catalogs is better run as a separate job.
How reliable are Sanity's webhooks?
Sanity delivers at-least-once: every request carries an idempotency key, failed deliveries are retried twice 30 seconds apart, the timeout is 30 seconds, and only one request per webhook runs at a time (as of October 2026). Your receiver has to handle duplicates and should verify the signature with the webhook secret.

Sources

Related articles

Open for select projects

Let's talk about your project

Book a no-obligation call, send us an email, or use the form – we'd love to hear from you.

150+
Completed projects
15
Years of experience
8
Senior‑level team members