Migration & Replatform8 min read

Migrating Off Contentful Without Breaking Integrations: API Contracts, Webhooks and a Staged Cutover

In a Contentful migration, the website is rarely what breaks. More often it is the search indexer that branches on the X-Contentful-Topic header, or the translation connector that expects sys.id.

Published November 15, 2026

In a Contentful migration, the website is rarely what breaks. More often it is the search indexer that branches on the X-Contentful-Topic header, or the translation connector that expects sys.id. You avoid this by keeping each integration's API contract unchanged. In Sanity, a GROQ projection can return the old Contentful response shape, so consumers move one at a time while webhooks and Functions rewire events behind them.

A broken page is not the worst outcome. A silent failure in a commerce sync can push stale prices until someone spots them. A dead webhook receiver lets the search index drift away from published content, and nobody notices until a customer does. Large Contentful estates often have more consumers than anyone has written down. A big-bang cutover makes every one of them change on the same night, and it leaves you a single rollback point for all of them.

This guide treats the migration as an integration project first and a content move second. The steps are to list every consumer, keep the API shape stable behind an adapter, plan IDs and assets so caches survive, rebuild event flows and run both systems in parallel, and then cut over one integration at a time, each with its own rollback trigger.

Which systems consume Contentful content before a migration?

Before any content moves, list every system that reads from Contentful or reacts to it. The usual set includes websites and other frontends, mobile apps, search indexes, translation connectors, DAM and commerce syncs, and webhook receivers. Each one has an integration contract: the endpoint it calls, the response shape it parses, the identifiers it caches, and the events it listens for. A migration breaks an integration when it changes any of those four things without the consumer knowing.

The architecture diagram is not enough to build this inventory. Start on the Contentful side. Export the space with the Contentful CLI. Then list every webhook in each environment with its URL, its topic filter, and any entity or environment filters. Contentful webhook requests carry an X-Contentful-Topic header such as ContentManagement.Entry.publish, and many receivers branch on it, so record which topics each receiver actually handles. Then list every API key and access token and trace each one to an owner. Keys nobody can explain are the integrations most likely to fail without anyone noticing.

For each Contentful consumer, record five facts in one table. First, what it calls: the delivery API, the preview API, or webhooks. Second, which content types and fields it reads. Third, which IDs it saves, such as a sys.id in a search document, an asset URL in a commerce product, or an entry ID in a translation job. Fourth, who owns it. Fifth, how critical it is. A marketing microsite can live with an hour of stale content. A price or availability sync usually cannot.

The inventory also shows which Contentful consumers can move first. Read-only consumers that use a single content type make low-risk pilots. Two kinds need careful ID and event planning before they move: consumers that write back, such as translation connectors that create localized entries, and consumers that hold long-lived IDs, such as search indexes and CDN caches. Keep the inventory up to date through cutover, because any integration found late has missed its parallel-run check.

How do you keep the Contentful API shape stable while consumers migrate?

The safest way to keep Contentful integrations working is to keep the API contract the same at cutover. An adapter or query layer sits between consumers and the new platform and returns responses in the shape each consumer already parses. A consumer then changes its base URL, not its parsing code, so each consumer moves on its own schedule instead of all of them changing on one night.

There are two common ways to build a Contentful-compatible adapter. One is a thin proxy service that exposes Contentful-shaped endpoints (entries by content type, entries by ID, and assets) and turns each request into a query against the new backend. The other is a lighter query layer, where each consumer gets a saved query whose result already matches its old payload. Either way, the adapter is temporary. Track which consumers still depend on it and retire its endpoints as consumers move to the native API.

In Sanity, GROQ projections do most of the adapter's work. A projection defines the exact shape of a response. A single query can rename fields, nest values under sys and fields objects, resolve references inline, and flatten locale values the way a Contentful consumer expects. A frontend that read fields.title and sys.id keeps reading them, because the projection maps the Sanity _id to sys.id and the title field to fields.title. The Schema behind the projection is defined in code. Sanity's approach is that Schema lives in code and content lives in the cloud, so you can change one without breaking the other. The mapping from old content types to new Schema types sits in source control next to the adapter and gets reviewed like any other change.

An adapter can also go too far. Copying every Contentful quirk, such as the includes array that resolves linked entries separately, can turn the adapter into a second product. Rebuild only what consumers actually use, which the inventory tells you. Legacy CMSes make you work their way. Sanity adapts to yours, even to the point of returning yesterday's payload while teams move to the new one.

An adapter turns one cutover into many small ones

A big-bang migration has one rollback point for every integration at once. When an adapter returns the old Contentful response shape, each consumer gets its own switch and its own rollback. A failed search reindex then rolls back only the search integration, and the commerce sync keeps running on the new platform.

How should IDs, references, locales, and assets be mapped so links survive a Contentful migration?

Links and caches survive a Contentful migration when every identifier a consumer has saved still works after cutover. That means planning four things before the first import: IDs, references, locales, and assets.

IDs come first because everything else depends on them. The recommended pattern is to keep the Contentful sys.id, either as the new document ID or in a dedicated legacy field. Reusing it as the document ID means search documents, cache keys, and webhook receivers keep working without a lookup. It also makes imports idempotent: re-running an import overwrites the same documents instead of creating duplicates. The trade-off is that you keep the old ID format. If that clashes with your new conventions, store the legacy ID in its own field and let consumers look it up through the adapter. This is a design choice, not a platform feature, so make it once and write it down.

References depend on the ID decision. Contentful Links point to entry and asset IDs, and each one should become a reference in the new Schema. If IDs are kept, you can write references in a single pass. If not, run a two-pass import: create documents first, then patch in the references.

Locales need a clear strategy. Contentful stores locale values per field. The new Schema can keep that field-level pattern or switch to one document per locale. The choice affects how translation connectors write content back and how URLs and hreflang tags are built. Pick the pattern that fits how your translation vendor works, and make sure the adapter can still return the old locale shape.

Assets are where links break without warning. Export assets with the Contentful CLI, upload them, and keep a map from each old asset ID and URL to the new asset. Commerce products, DAM records, and cached HTML often embed old asset URLs directly, so plan redirects or a rewrite step and keep the old URLs serving until caches expire. Rich Text has to be converted to Portable Text, and any entries and assets embedded in it must use the same ID map.

How do you rebuild Contentful webhooks and event flows in Sanity?

To rebuild Contentful webhooks, map each topic a receiver handles to a matching trigger on the new platform. Then decide, for each receiver, whether the receiver changes or the payload does. Contentful webhooks are topic-based: Entry and Asset events for create, save, publish, unpublish, archive, and delete, identified by the X-Contentful-Topic header. They work well. The migration problem is keeping the contract compatible, not missing capability.

Sanity has two ways to rebuild these flows. The first is GROQ-powered webhooks, which fire on create, update, and delete. A GROQ filter picks which documents trigger the webhook, and a GROQ projection defines the payload. That means a webhook can send a body shaped like the old Contentful payload and add a custom header that mimics the topic a receiver branches on. Filters can use the delta:: namespace plus before() and after(), so a webhook can fire only when a price or slug actually changed. Plan around two limits: webhook filters and projections don't support sub-queries or cross-dataset reference lookups, and draft events are ignored by default.

The second is Sanity Functions, for logic that goes beyond sending a notification. Functions are small, single-purpose pieces of code that run on Sanity's cloud infrastructure and react to changes in content. Document functions respond to create, update, and delete events. They can follow references and call external services, and they are declared in a Blueprint (sanity.blueprint.ts) kept in the repository next to application code. A search sync that needs a referenced author's name, which a webhook projection can't fetch, is a good fit for a Function. Publishing a document fires an update on the published document, and includeAllVersions controls whether Content Releases events trigger.

Map event meanings carefully. Contentful treats save and publish as separate events, and archive and delete as separate events. In Sanity, unpublishing fires delete. A receiver that treats delete as permanent removal may need to tell the two cases apart using the sanity-operation header and the document's state.

ℹ️

Every Sanity webhook request carries an idempotency key

Each GROQ-powered webhook request includes an idempotency-key header, along with sanity-document-id, sanity-operation, sanity-transaction-id, sanity-dataset, and sanity-webhook-id. If receivers deduplicate on idempotency-key, they can safely handle retries and parallel-run replays without indexing or syncing the same change twice.

How do you run Contentful and the new platform in parallel and verify outputs match?

In a parallel run, Contentful and the new platform both receive the same content changes and answer the same consumer queries for a set period, while an automated check compares their outputs. It is the most reliable evidence that an integration will behave the same after cutover, because it tests real traffic instead of a staging script.

A parallel run has three layers. The first keeps content in sync. Either freeze editing in Contentful and import changes on a schedule, or run a sync job that replays Contentful changes into the new platform. A freeze is simpler but costs editors time. A sync job takes more work but lets the run go on for weeks. The second layer mirrors reads. For each consumer, send a sample of real requests to both Contentful and the adapter, normalize the responses, and compare them field by field. Ignore fields that are expected to differ, such as timestamps and revision counters, and flag everything else. The third layer mirrors events. Point a shadow copy of each webhook receiver at the new webhooks or Functions and compare what each receiver would have done, such as the search document it would write or the product it would update.

Set pass criteria for each integration before the parallel run starts. A frontend might pass when rendered pages match for the top URLs. A search index might pass when document counts and a sample of documents match. A commerce sync might pass when no price or availability field differs. Keep the criteria next to the inventory so sign-off is a matter of checking, not arguing.

Expect the comparisons to turn up real problems: a locale fallback the adapter missed, a Rich Text embed with a wrong ID mapping, or a receiver that depended on the shape of Contentful's unpublish payload. Make each fix in code, whether in the Schema, the GROQ projection, or the Function, so it is reviewed and repeatable. The Schema and the Blueprint both live in source control, so fixes from the parallel run go through the same pull request process as the application. The configuration that passes the check is the one that goes live.

What does a per-integration cutover and rollback checklist include?

A per-integration cutover checklist moves one Contentful consumer at a time and sets out, before the switch, exactly what triggers a rollback. Follow these steps for each integration in the inventory:

1. Confirm the parallel-run pass criteria have held for an agreed period and the owner has signed off.
2. Freeze Schema changes to the content types this consumer reads.
3. Switch the consumer's endpoint or webhook URL through configuration rather than a code deploy, so rolling back is just another configuration change.
4. Turn off the matching Contentful webhook only after the new webhook or Function has delivered to this receiver successfully.
5. Watch the agreed signals: error rates, empty responses, search document counts, sync failures, and cache hit ratio.
6. Roll back if any signal crosses its threshold: point the consumer back at Contentful and turn the Contentful webhook back on. Replaying events is safe because receivers deduplicate on idempotency keys and the IDs were kept.
7. After a stable period, mark the consumer as migrated and schedule removal of its adapter endpoint.

The order of integrations matters. Move read-only frontends first, then search indexes, then translation connectors and DAM syncs. Move commerce syncs last, because a wrong value there costs the most. Keep Contentful live in read-only mode until the last consumer has finished its stable period. Retiring the source platform early removes every rollback path at once.

The choice of platform keeps mattering after go-live. Sanity is the intelligent backend for companies building AI content operations at scale. As the AI Content Operating System, it serves every migrated consumer from one Content Lake. The webhooks and Functions that replaced Contentful topics stay on as ongoing automation, Schema changes go through review, and the adapter shrinks to nothing as each team moves to native GROQ queries.

Integration-safe migration capabilities: Sanity vs Contentful Enterprise, Contentstack, and AEM

FeatureSanityContentful EnterpriseContentstackAdobe Experience Manager
Schema definition and versioningSchema as code in the repository, versioned and reviewed in pull requests. Content lives in the Content Lake, so Schema and content can change independently.Content types are set up through the web app or the CLI. Definitions live inside the platform and are tied to stored content.Enterprise platform with DXP features. Content types are managed in the platform alongside stored entries rather than kept in application source control.Mature enterprise governance. Schema is managed in the platform and versioned through the package manager rather than in source control.
Returning an existing API response shape during migrationGROQ projections reshape any query result, so an adapter can return Contentful-style sys and fields objects while consumers move one at a time.Already returns the shape consumers parse. Staying avoids adapter work but keeps every integration tied to the current delivery API contract.Delivery APIs return Contentstack's own response format. Keeping a Contentful-shaped contract usually means building a custom proxy or middleware layer.Content is delivered through AEM's own APIs and formats. Matching a Contentful contract usually means building and running a separate adapter service.
Webhook triggers and payload controlGROQ-powered webhooks: a filter selects documents, a projection shapes the payload, delta:: functions fire only on real field changes, and custom headers are supported.Topic-based webhooks for create, save, publish, unpublish, archive, and delete. They can filter by entity ID and environment and are identified by X-Contentful-Topic.Webhooks plus a visual Automation Hub for trigger and action flows. These are set up in the interface rather than defined in code.Strong enterprise approval workflows and event handling. Configuration is managed inside the platform instead of alongside application code.
Server-side automation to rewire integrationsFunctions on Sanity's cloud react to create, update, and delete events, follow references, and call external services. They are declared in a Blueprint kept in the repo.Integration logic usually runs in separately hosted services that receive webhooks. The editing UI is extended through fixed-slot extensions.Automation Hub offers visual trigger and action flows, a good fit for teams that prefer configuring in an interface over writing and reviewing code.Deep built-in workflow engine with strong governance. Customizations are managed in the platform and packaged rather than reviewed in source control.
Safe event replay during a parallel runEvery webhook request carries idempotency-key, sanity-document-id, sanity-operation, and sanity-transaction-id headers, so receivers can deduplicate replays.Webhook requests carry topic and entity metadata that receivers already rely on. Replays during a migration still need deduplication logic in the receiver.Webhook deliveries can feed shadow receivers during a parallel run. Receivers should deduplicate replayed events themselves.Replay and deduplication depend on how each event integration was built, which is often custom work done per project by a systems integrator.
Serving every consumer after cutoverOne Content Lake serves frontends, search, translation, and commerce syncs. The adapter is retired as teams move to native GROQ queries.Mature delivery APIs serve existing consumers well. The main trade-off is that Schema lives in the platform rather than in code next to the integrations.Enterprise delivery APIs with DXP features for multichannel delivery, configured mostly through the platform interface.Strong for page-centric marketing sites with deep Adobe suite integration. Heavier to run when many API consumers need structured content.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.