Now booking enterprise content platform builds for 2026. Contact us

CMS migrations Contentstack

Migrate from Contentstack to Payload

A working reference for moving a Contentstack stack onto Payload: a field-by-field mapping table, how modular blocks and global fields translate, and the export-and-import pipeline that runs in practice.

  • From Contentstack
  • To Payload
Payload Partner · Top Contributor We build and maintain Payload plugins used by the wider community. Direct access to the maintainers when something needs attention.

A Contentstack migration is mostly a structural translation. The content-type schema maps onto Payload collections cleanly, and the work concentrates in three Contentstack-specific constructs: modular blocks, global fields, and the JSON rich text editor. WAYF has built this migration in production — the Ingersoll Rand frontend ran on Contentstack, and we decoupled it behind a content-provider contract with adapters for both Contentstack and Payload, so the mapping below is drawn from a real move rather than a spec read.

WAYF is a Payload Partner agency and a top contributor to its open source.

Field mapping

Each Contentstack content type becomes a Payload collection, each field a Payload field. The table covers Contentstack’s field set.

Contentstack fieldPayload fieldNotes
Single line textboxtextDirect
Multi line textboxtextareaDirect
Rich text editor (JSON RTE)richText (Lexical)Tree transform — see below
MarkdownrichTextConvert the Markdown body
HTML / customrichTextHTML-to-Lexical import
NumbernumberDirect
BooleancheckboxDirect
DatedateISO in; check timezone handling
File (asset)uploadMigrate assets first
Linkgroup (title, href)Contentstack stores a title + URL pair
ReferencerelationshipCarries uid + content type — two-pass
GroupgroupNested field group
Group (multiple)arrayOne row per instance
Global fieldreusable groupDefine the field set once, share it
Modular blocksblocksThe natural fit — see below
Select (dropdown)selectCarry the choice list into options
JSONjsonDirect

The rows that carry the work are references, assets, modular blocks, and global fields.

Modular blocks and global fields

Two Contentstack constructs need a deliberate mapping decision before any data moves.

Modular blocks are Contentstack’s page-builder field: an ordered list where an editor stacks sections of different shapes. Each entry names its block type. This is the same shape as Payload’s blocks field — define one Payload block per Contentstack block type, mirror the subfields, and the data migrates straight across. On a marketing stack where most pages are modular-block compositions, building these block definitions is the bulk of the schema work.

Global fields are reusable field groups shared across content types — an SEO group reused on every page type, say. Model the global field once in Payload, as a shared field array imported wherever the content type used it, so the reuse survives the move instead of being copy-pasted into each collection.

References and the content type

A Contentstack reference field stores an array of links, each carrying the referenced entry’s uid and its _content_type_uid:

"author": [{ "uid": "blt9f2...", "_content_type_uid": "author" }]

The uid is a Contentstack identifier; Payload assigns its own document ID at creation. So references resolve on a second pass, after every entry exists. The _content_type_uid tells you which Payload collection the relationship points at, which matters when a reference field can target more than one type — map it to a Payload relationship with multiple relationTo collections.

How the migration runs

Export the stack

Two routes:

  • Contentstack CLI (csdx cm:stacks:export) writes content types, global fields, entries, assets, environments, and locales to a local folder. Good for a full, repeatable export.
  • Content Management API (CMA) when reading a live stack. Page through each content type’s entries, including references and embedded assets:
async function* readEntries(contentType: string) {
  let skip = 0;
  for (;;) {
    const res = await fetch(
      `https://api.contentstack.io/v3/content_types/${contentType}/entries` +
        `?include_count=true&limit=100&skip=${skip}`,
      { headers: { api_key: API_KEY, authorization: MANAGEMENT_TOKEN } },
    );
    const { entries, count } = await res.json();
    yield* entries;
    skip += entries.length;
    if (skip >= count) break;
  }
}

Migrate assets first

Reference and file fields point at assets, so assets exist before entries. Pull each asset’s binary, create a Payload upload, and record the ID map:

const buffer = Buffer.from(await (await fetch(asset.url)).arrayBuffer());
const created = await payload.create({
  collection: "media",
  data: { alt: asset.title ?? "" },
  file: { data: buffer, name: asset.filename, mimetype: asset.content_type, size: buffer.byteLength },
});
assetIdMap.set(asset.uid, created.id);

Import entries, then resolve references

Pass one creates entries with reference fields blank and records contentstackUid → payloadId. Modular blocks and groups travel inline with the entry, but any reference or asset inside a block still needs remapping — assets on pass one via the asset map, references on pass two. Pass two updates each entry, swapping stored uids for Payload IDs.

Rich text: JSON RTE or HTML

Contentstack’s JSON Rich Text Editor stores a tree, close in spirit to Lexical but with its own node names. Walk the JSON RTE tree and emit Lexical: p → paragraph, h1h6 → heading, ol/ul → list, blockquote → quote, a → link, img and embedded reference nodes → upload or block nodes resolved through the asset and entry maps. Text styling is a set of boolean attributes (bold, italic, underline, strikethrough, inlineCode) on each text node, which become Lexical’s format bitmask:

const FORMAT = { bold: 1, italic: 2, strikethrough: 4, underline: 8, inlineCode: 16 } as const;

Stacks on the older HTML-based RTE need an HTML-to-Lexical import instead. Either way, create one document by hand in the Payload admin and read its stored richText value to confirm the exact node shape your transformer must produce — it can shift between Payload versions.

Localization

Contentstack keeps a master locale plus additional locales, with each entry localized per locale and an unset field falling back to master. Payload keeps locale variants under one document ID, selected by a locale argument on each write. Fetch each entry per locale (?locale=fr-fr), import the master locale first to create the Payload document, then update the same document for each additional locale. Map the locale codes up front; Contentstack’s fr-fr is rarely the same string as your Payload locale identifier.

Environments and publishing

Contentstack publishes entries to environments — separate targets such as development, staging, and production, each with its own published version and publish_details. Decide which environment is the source of truth for the migration, usually production, and migrate that published content. Map the publish state onto Payload’s drafts via its versions feature, so an unpublished entry lands as a draft rather than going live on import.

What it costs

A stack with a handful of content types, a few global fields, no deep modular-block nesting, and a single locale is three to five engineering days: schema derivation, the export, the asset pipeline, and a cutover rehearsal.

A large marketing stack — many modular-block types, nested groups, references across several content types, the JSON RTE with embedded entries, and multiple locales — is a three-to-four-week migration, with the block definitions and the RTE transform as the long poles. We sized exactly this shape on the Ingersoll Rand frontend. A scoped discovery is the place to size yours.

If you want an honest read on what moving your specific Contentstack stack involves, a 25-minute call is the fastest way to get one.


Rather have it done for you? WAYF runs Contentstack to Payload migrations end to end.

We're booking content platform
engagements for 2026.

Twenty-five minutes to walk through the work and decide if we're the right team for it. Scoping and a fixed price come after.