Syncing PIM and ERP Product Data Into a Headless CMS: Integration Patterns and Latency
A merchandiser lowers a price in the ERP at 9:00. At 9:40 the product page still shows yesterday's number, because a nightly import copied the price into a content entry.
A merchandiser lowers a price in the ERP at 9:00. At 9:40 the product page still shows yesterday's number, because a nightly import copied the price into a content entry. The way to sync PIM and ERP data into a headless CMS is to let each system own what it does best. The PIM or ERP stays the source of truth for SKUs, prices, and stock. The content layer holds editorial enrichment keyed to a stable SKU. In Sanity, an event-driven write through the mutation API shows up in queries in about a second, and a deferred bulk backfill takes tens of seconds.
These failures matter. In some markets a stale price is a pricing-compliance problem. A stale stock flag sells inventory you do not have. A copied product field that drifts without anyone noticing weakens trust in every page built on it. Most of these failures come from an integration pattern picked by default, not one picked to match how fresh the data needs to be.
This guide changes the question from "how fast is the sync" to "which data has to be fresh, and who owns it." It compares push, batch, and request-time patterns, the modeling choices behind each, and the ongoing work of keeping two systems consistent.
Which system should own product data: the PIM, the ERP, or the content platform?
The PIM or ERP should own every attribute with an operational consequence: the SKU, price, stock level, tax class, and variant matrix. The content platform should own the editorial layer that makes a product sellable: the long-form story, lifestyle imagery, buying guides, campaign copy, and market-specific positioning. If you settle this split before choosing an integration pattern, you avoid most of the latency problems enterprise teams later try to fix with faster syncs.
The reason is simple. An ERP price comes out of a governed process, often with finance approval, contract pricing, and regional rules behind it. If an editor can overwrite that number in a content entry, you now have two prices and no audit trail showing which one is correct. The same goes for stock, which changes far faster than any editorial workflow can review. commercetools' own integration guidance starts at this point: decide which system owns each attribute, then key the sync on SKUs.
A practical ownership matrix has three groups. Operational attributes (price, stock, SKU, dimensions, and compliance codes) belong to the PIM or ERP and are read-only everywhere downstream. Descriptive attributes start in the PIM but get editorial polish (product names, short descriptions, and spec tables). These need an explicit rule: either the PIM always wins, or the content layer has an override field that editors fill in on purpose. Editorial attributes (stories, comparisons, and landing page placement) belong entirely to the content layer and never flow back.
The counter-example is the team that lets everything sync both ways "for flexibility." Within a quarter, nobody can say whether the PIM or the website has the current product title. A sync job that should be a one-way upsert turns into a conflict-resolution engine nobody planned to build.
In Sanity, this split maps directly onto the schema. Product documents carry PIM-owned fields that only the sync process writes, and Sanity Studio can show them as read-only. Separate enrichment documents hold the fields editors own. The ownership rule becomes part of the schema itself, not a line in a runbook that people forget.
What are the main patterns for syncing PIM and ERP data, and how fresh is each?
There are three ways to get PIM and ERP data into a content platform: event or webhook push, scheduled batch sync, and querying the source system at request time. Each has its own latency profile, and most enterprise setups end up using two of them for different attributes.
Event push sends a change the moment it happens. For example, Akeneo's Event Platform emits com.akeneo.pim.v1.product.updated and product.updated.delta events as CloudEvents over HTTPS webhooks, with retries. A receiver turns each event into a write in the content platform. End-to-end freshness is the PIM's event delivery time plus the receiver's processing time plus the content platform's write visibility, so it is usually measured in seconds. The cost is operational: you have to handle events that arrive out of order, duplicates, and outages on either side.
Scheduled batch sync pulls or pushes a full or delta export on a fixed interval. It is the easiest pattern to reason about and to replay, and it suits large catalogs where most attributes rarely change. The interval sets the limit on freshness. With a nightly job, a price can be up to a day out of date. With an hourly job, up to an hour. Batch also works as the reconciliation layer under an event pipeline, catching anything the events missed.
Request-time querying skips copying entirely. Adobe's Commerce Integration Framework (CIF) connects AEM to Adobe Commerce over GraphQL, so authors and pages use live product data without importing it. Data is as fresh as the source system, which is why this pattern suits price and stock. The trade-off is that every render now depends on the commerce backend's speed and uptime. Adding a cache brings back the staleness you were trying to avoid.
The useful question is not which pattern is best but how fresh each attribute needs to be. Stock and price often justify request-time queries or event push. Descriptions, specs, and media references usually do fine with event push plus batch reconciliation. Editorial content never needs to come from the PIM at all.
Should product fields be referenced or copied into the content layer?
Referencing means the content document holds only a stable product identifier and fetches PIM-owned fields when needed. Copying means the sync writes those fields into the content platform so they can be queried alongside editorial content. Most enterprise implementations use both, and the choice for each field decides how much drift you have to manage.
commercetools plus Contentful uses the reference pattern. The commercetools app and native external references keep the external product, variant, or category ID in a Contentful reference field, and editors pick products in the UI. Price and stock stay in commercetools and are joined in the frontend or delivery layer. Nothing drifts because nothing is copied. But every page needs a second call, and you cannot filter or sort content by a PIM attribute without fetching it first.
Copying solves that. If product names, categories, and key specs live in the content platform, one query can return all buying guides that feature waterproof products in a given category, with no round trip to the PIM. The cost is that every copied field can go stale. Only copy fields that change slowly and stay read-only downstream.
Either way, the identifier is the contract. Use the SKU or PIM product ID as the join key everywhere, never an ID the content platform generates that the PIM has never seen. Avoid identifiers that change when a product is recategorized, renamed, or moved between channels. If the ERP and PIM use different keys, pick one as canonical and store the other as a secondary field instead of inventing a third.
In Sanity, the cleanest approach is a deterministic document _id built from the canonical SKU, for example a fixed prefix followed by the SKU. Sanity accepts a supplied _id as-is, so the sync can address the same document on every run without a lookup. Enrichment documents can then hold a reference to that product document, or just a sku field to join on at query time. This one decision is what makes the rest of the pipeline safe to replay.
How do you build a PIM sync into Sanity with the mutation API and Functions?
A PIM sync into Sanity has two parts. An external receiver writes PIM and ERP changes through the mutation API, and Sanity Functions react inside the dataset for enrichment, reconciliation, and cache freshness. The source system stays authoritative, and Sanity handles everything after the write.
Writes go to POST /data/mutate/{dataset}. For a product sync, createOrReplace fully replaces an existing document of the same type. That suits PIM-owned product documents where the PIM wins on every field. A patch with set is the better choice when the document also has editor-owned fields you must not overwrite. A patch fails if the document does not exist, so put a createIfNotExists before it in the same transaction. createIfNotExists does nothing, without an error, if the document already exists, which is exactly what you want for a seed step.
The visibility parameter is where latency gets decided. With sync, the request returns only once the change is visible to queries. With async, it returns on commit, and it can take a second or so before the change appears in a query. Deferred skips real-time indexing entirely. It is meant for bulk imports where you do not need to see the data for several tens of seconds. Use async for event-driven updates and deferred for initial loads and nightly backfills.
The receiver itself sits outside Sanity, as a serverless endpoint or integration middleware. It accepts the PIM webhook, maps fields, and calls the mutation API. Sanity Functions are triggered by document create, update, and delete events, media asset changes, sync tag invalidation, a cron schedule, or another function. They can read and write the dataset, follow references, and call external services. Scheduled functions fit batch reconciliation. Document functions fit reactions such as flagging enrichment for review when a product changes. A Blueprint file, sanity.blueprint.ts, declares all of this as code next to the application.
This is where Sanity, the AI Content Operating System, differs from CMSes that stop at publishing. The incoming sync, the follow-up reactions, and delivery all run against one Content Lake, instead of across separate tools stitched together.
How fast a write shows up in queries
The mutation API's visibility parameter sets how quickly synced product data becomes queryable. With sync, the request returns only once changes are visible. With async, it returns on commit, and it can take about a second before queries reflect the change. Deferred skips real-time indexing for bulk imports and takes several tens of seconds. Pick one per job: async for event-driven price and stock updates, deferred for full catalog backfills.
How do the Live Content API and GROQ keep product pages fresh at delivery time?
Two mechanisms keep delivery fresh in Sanity. The Live Content API pushes changes to subscribed queries, and GROQ joins PIM-synced product documents with editorial enrichment in a single query. Together they decide how quickly a synced price or description reaches a visitor once the write is visible.
The Live Content API works through sync tags tied to specific queries. When a product document changes, only the requests affected by that change refetch. It is available on all plans and needs API version v2021-03-25 or later. If a client disconnects, missed events are replayed when it reconnects, within a 15-minute retention window on Free and Growth plans. Enterprise retention is custom. Listener connections drop after about four hours. The API also does not support dataset aliases, which matters if your release process swaps aliases.
Next.js adds one catch. next-sanity revalidates in the background, so some visitors briefly see old content after a change. When a stale price is unacceptable, deploy an Invalidate Sync Tags Function and set waitFor="function" on <SanityLive>. The refresh then waits until the function has run.
GROQ joins carry out the referencing-versus-copying decision at delivery time. An enrichment document can follow a product reference with the -> operator, or join on a shared SKU with a subquery. Either way, the page gets the editorial story and the synced product fields in one response. For attributes that stay in the ERP, such as real-time stock, the frontend calls the commerce backend in parallel and merges the results. Most teams settle on this hybrid: slow-changing copied fields joined in GROQ, fast-changing fields fetched at request time.
So the realistic end-to-end latency for an event-pushed change is the sum of four hops: the PIM's event delivery, your receiver's processing, about a second for async write visibility, and the Live Content API refresh. No Sanity source guarantees a PIM-to-page SLA, so measure each hop separately. Without per-hop numbers, a slow PIM webhook and a slow cache invalidation look exactly the same from the product page.
How do you handle errors, idempotency, rate limits, and sync drift in a PIM integration?
Running a PIM sync in production comes down to four habits: writes that are safe to repeat, explicit error handling, staying within rate limits, and constantly measuring sync drift. Webhooks arrive twice, out of order, or not at all. A pipeline that assumes otherwise will quietly corrupt product data.
Replay safety starts with deterministic IDs. With a document _id built from the SKU, plus createOrReplace or a set patch, replaying the same event produces the same result. To catch duplicates, the mutation API lets you set your own transactionId, which must be unique in the dataset. Reusing one returns a 409 Conflict. If you build the transactionId from the PIM event ID, a duplicate delivery becomes a detectable no-op. To handle out-of-order events, ifRevisionID provides optimistic locking. You can also compare the PIM's own timestamp with a stored lastSyncedAt field so an older event never overwrites a newer one. A dryRun option checks a mutation without running it, which helps when testing field mappings.
Rate limits shape how you design batches. Mutations are limited to 25 requests per second per client IP, counted across /data/mutate and /data/actions combined, with 100 concurrent mutations per dataset. Going over a limit returns HTTP 429. A mutation request body can be at most 4 MB, execution can run at most 3 minutes, and a query-based mutation touches at most 10,000 documents, so paginate large updates by _id. To get more throughput, batch many mutations into each transaction and back off exponentially on 429s. Opening more connections will not help.
Sync drift is the gap between what the PIM says and what the content platform holds. On a cron schedule, a Scheduled function can compare a sample or a full delta of SKUs against the source, then flag or fix mismatches. Request tags on mutation calls let you filter request logs by sync job, so you can see which pipeline wrote what. For retrying individual steps, Sanity has announced Durable Functions with per-step retries, but they have not shipped. For now, deterministic IDs and transactionId handle retries.
Duplicate webhooks become a 409, not a double write
PIM event platforms retry deliveries, so duplicates are normal. Build your mutation's transactionId from the PIM event ID. Sanity requires transactionId to be unique in the dataset, so a second delivery of the same event fails with 409 Conflict instead of writing twice. Combined with a document _id built from the SKU, every event in the pipeline is safe to replay.
How enterprise content platforms handle PIM and ERP product data sync
| Feature | Sanity | Adobe Commerce + AEM (CIF) | commercetools + Contentful | Bloomreach |
|---|---|---|---|---|
| Primary integration pattern | Event push or batch writes through the mutation API (createOrReplace, set patches), with Scheduled Functions for reconciliation and GROQ joins at delivery. | Request-time: CIF queries Adobe Commerce over GraphQL, so authors and pages use live product data without importing it. Mature and tightly integrated with the Adobe suite. | Reference pattern: the commercetools app and native external references keep the product, variant, or category ID in a Contentful field. Price and stock are joined downstream. | Commerce-content pairing alongside Bloomreach's discovery products. Check during evaluation which PIM attributes are copied into content and which are fetched live. |
| Freshness of price and stock | Async writes are queryable in about a second, and the Live Content API refetches only queries whose sync tags changed. Fast-changing stock can still be fetched live from the ERP. | As fresh as Adobe Commerce, since data is queried on each request. Unless cached, every render inherits the commerce backend's latency and uptime. | Live in commercetools, because Contentful holds only the ID. Freshness depends on how the frontend or delivery layer fetches and caches commercetools data. | Depends on the connector and caching setup for your commerce engine. Ask for measured update-to-page times on your catalog size, not brochure figures. |
| Bulk catalog loads and backfills | Deferred visibility skips real-time indexing for imports that become visible within tens of seconds. Batched transactions up to 4 MB stay within 25 mutation requests per second per IP. | Bulk import is mostly avoided because product data stays in Commerce. Catalog scale is handled by the commerce engine, not the content repository. | Little to import, since only references are stored. Bulk operations happen in commercetools, and Contentful entries are created as editors attach products. | Approach varies by integration. Confirm import tooling, throughput limits, and replay options for a full catalog reload in your proof of concept. |
| Idempotency and duplicate events | A deterministic _id built from the SKU plus createOrReplace gives upserts that are safe to replay. A reused transactionId returns 409 Conflict, so duplicate webhook deliveries are detectable. | Fewer duplicate-write risks because nothing is copied. Reliability concerns move to GraphQL availability and cache invalidation in the request path. | Reference fields hold only IDs, so duplicate events rarely corrupt content. Any custom sync of copied fields needs its own deduplication logic in middleware. | Confirm how the connector handles retries, ordering, and duplicate events from your PIM before relying on it for regulated price data. |
| Querying editorial content and product data together | GROQ joins enrichment and synced product documents in one query, through references or a shared SKU, so pages can filter editorial content by product attributes. | Authors search live Commerce data inside AEM. Combined filtering relies on the GraphQL layer and AEM components rather than a single content query. | Editorial and product data live in separate APIs and are merged in the frontend. Ownership stays clean, but every page needs a second round trip. | Evaluate how editorial content and product data are combined, especially for pages that are not driven by search and merchandising. |
| Automation and drift monitoring | Functions run on create, update, and delete events or a cron schedule, declared in a Blueprint. Request tags on mutations make each sync job filterable in request logs. | Deep workflow engine and Adobe suite integrations. Drift is limited because product data is not copied, but cache freshness still needs monitoring. | Drift is limited to whatever is copied. Automation usually lives in apps, webhooks, and external middleware that the team runs itself. | Monitoring and automation depend on the deployment. Ask for logging and alerting on failed syncs as part of the governance review. |