Skip to main content
Skip to main content

Contentful Plugin

In this document, you’ll learn how to install and use the Contentful plugin.


Contentful is a headless CMS service that allows developers to integrate rich CMS functionalities into any platform.

By integrating Contentful to Medusa, you can benefit from powerful features in your ecommerce store including detailed product CMS details, easy-to-use interface to use for static content and pages, localization, and much more. The data is also automatically synced between Contentful and your Medusa store.


Medusa Components

This guide assumes you already have a Medusa backend installed. If not, you can learn how to install it here.

Redis must also be configured in your Medusa backend if you want to sync data from Medusa to Contentful. Learn how to configure it here.

Needed Account

  • Contentful account with a space created. A space is created by default when you create a new account.

Install Plugin

In the directory of your Medusa backend, run the following command to install the Contentful plugin:

npm install medusa-plugin-contentful

Next, add the plugin into the plugins array in medusa-config.js:

const plugins = [
// ...
resolve: `medusa-plugin-contentful`,
options: {
space_id: process.env.CONTENTFUL_SPACE_ID,
access_token: process.env.CONTENTFUL_ACCESS_TOKEN,
environment: process.env.CONTENTFUL_ENV,

Plugin Options

The plugin accepts the following options:

  1. space_id: (required) a string indicating the ID of your Contentful space. Refer to Contentful’s documentation if you’re unsure where to find it.
  2. access_token: (required) a string indicating the personal access token for content management. Refer to Contentful’s documentation to learn how to create it.
  3. environment: (required) a string indicating the Contentful environment. Typically, its value should be master.
  4. ignore_threshold: (optional) a number indicating the number of seconds to wait before re-syncing a specific record. By default, its value is 2.
  5. custom_<TYPE>_fields: (optional) an object that allows you to map fields in Medusa to custom field names. Learn more here.

Make sure to add required values as environment variables:


Custom Field Mapping

When the plugin syncs data between Contentful and Medusa, it expects a set of fields to be defined in the respective content models in Contentful. If you choose to use different names to define those fields in Contentful, you have to specify them in the custom_<TYPE>_fields option mentioned earlier, where <TYPE> is the name of the content model.

For example, to change the name of the product’s title field, pass the following option to the plugin:

const plugins = [
// ...
resolve: `medusa-plugin-contentful`,
options: {
space_id: process.env.CONTENTFUL_SPACE_ID,
access_token: process.env.CONTENTFUL_ACCESS_TOKEN,
environment: process.env.CONTENTFUL_ENV,
custom_product_fields: {
title: "name",

The rest of this section includes the field names you can customize using this option for each content model type.


Migrate Content Models

In your Contentful space, you must have content models for Medusa entities such as products and regions.

You can either create the content models manually, or you can write a migration script in the Medusa backend that migrates these content models into Contentful.

This section includes migration scripts for Medusa’s entities that are relevant for Contentful. You can customize the scripts if necessary. You can also create scripts for custom content models, such as a Link or Navigation Item content model.

Before creating the migration scripts, run the following command in the root of your Medusa backend to install Contentful’s migration SDK:

npm install --save-dev contentful-migration
product Content Model
productVariant Content Model
collection Content Model
productType Content Model
region Content Model

Finally, create a loader at src/loaders/index.ts with the following content:

import { 
} from "@medusajs/medusa"
import { runMigration } from "contentful-migration"
import {
} from "./contentful-migrations/product"
import {
} from "./contentful-migrations/product-variant"
import {
} from "./contentful-migrations/product-collection"
import {
} from "./contentful-migrations/product-type"
import {
} from "./contentful-migrations/region"

type ContentfulPluginType = {
resolve: string
options: {
space_id: string
access_token: string
environment: string

export default async (
container: MedusaContainer,
config: ConfigModule
): Promise<void> => {
// ensure that migration only runs once
const storeService = container.resolve<StoreService>(
const store = await storeService.retrieve()

if (store.metadata?.ran_contentful_migrations) {
}"Running contentful migrations...")

// load Contentful options
const contentfulPlugin = config.plugins
.find((plugin) =>
typeof plugin === "object" &&
plugin.resolve === "medusa-plugin-contentful"
) as ContentfulPluginType

if (!contentfulPlugin) {
"Didn't find Contentful plugin. Aborting migration..."

const options = {
spaceId: contentfulPlugin.options.space_id,
accessToken: contentfulPlugin.options.access_token,
environment: contentfulPlugin.options.environment,
yes: true,

const migrationFunctions = [
name: "Product",
function: productMigration,
name: "Product Variant",
function: productVariantMigration,
name: "Product Collection",
function: productCollectionMigration,
name: "Product Type",
function: productTypeMigration,
name: "Region",
function: regionMigration,

await Promise.all( (migrationFunction) => {`Migrating ${
} component...`)
try {
await runMigration({
migrationFunction: migrationFunction.function,
})`Finished migrating ${
} component`)
} catch (e) {
if (
typeof e === "object" && "errors" in e &&
Array.isArray(e.errors) &&
e.errors.length > 0 &&
e.errors[0].type === "Invalid Action" &&
e.errors[0].message.includes("already exists")
) {`${
} already exists. Skipping its migration.`)
} else {
throw new Error(e)

await storeService.update({
metadata: {
ran_contentful_migrations: true,
})"Finished contentful migrations")

Notice that in the script you store a flag in the default store’s metadata attribute to ensure these migrations only run once.

Setup Webhooks

As mentioned in the introduction, this plugin supports two-way sync. A subscriber listens to changes in the data, such as adding a new product, and syncs the data with Contentful.

To update the Medusa backend when changes occur in Contentful, you must configure webhooks settings in Contentful.


For webhooks to work, your backend must be deployed and accessible publicly. If you haven’t deployed your backend, refer to these deployment guides.

To do that:

  1. On your Contentful Space Dashboard, click on Settings from the navigation bar, then choose Webhooks.
  2. Click on the Add Webhook button.
  3. In the form, enter a name for the webhook.
  4. In the URL field, choose the method POST and in the input next to it enter the URL <BACKEND_URL>/hooks/contentful where <BACKEND_URL> is the URL of your deployed Medusa backend.
  5. Scroll down to find the Content Type select field. Choose application/json as its value.
  6. You can leave the rest of the fields the same and click on the Save button.

Test the Plugin

Run the following command to start your Medusa backend and test the plugin:

npx medusa develop

If you created migration scripts, they’ll run when the Medusa backend first starts and migrate your content models to Contentful. You can go to your space’s dashboard to confirm they’ve been created.

After that, try the sync functionality by creating or updating products in the Medusa backend. If you’ve also setup webhooks, you can test out the sync from Contentful to Medusa.


As mentioned in the Prerequisites section, you must configure Redis for the Medusa to Contentful sync to work.

What’s Next?

After installing the plugin, you can either customize the Next.js storefront to fetch data from Contentful, or build a storefront that connects to both Medusa and Contentful.

Was this section helpful?