Developer Demo

External Content Sync

Push any external data source directly into Optimizely Graph without touching the CMS. Define a schema once via the Content Source sync API, send NdJSON over HTTP, and your data is instantly queryable alongside CMS-managed content - same GraphQL endpoint, same ISR caching. When the CMS itself is the source of truth, push into the CMS instead so editors can edit it - see Management API.
◎ Demo data - run npx tsx scripts/seed-quotes.tsGraph Content Source APINdJSON · PUT types · POST dataISR · 60s + on-demand tag

Two Ways In - CMS-native vs External #

There are two ways to make data queryable in Optimizely Graph, and they are not interchangeable. Both end up on the same GraphQL endpoint, but they differ in who owns the data and whether editors can edit it. Choose based on where the source of truth lives - not on which API is easier to call.

Push into the CMS

Management API / Visual Builder

Editable · CMS-native

The content is authored via the Management API (v1/content) or directly in Visual Builder, and lives entirely inside the CMS. Editors own it and can change any field.

  • Editors author and edit every field
  • Draft / review / publish lifecycle
  • Preview mode - CMS inline edit + preview
  • FX experiment targeting via CMS Variations
  • Per-locale values on isLocalized fields
Use when the CMS is the source of truth. See Management API ↗ for the seeding and authoring workflow.

Push into Graph

Content Source API - this page

Read-only · External

Data is pushed straight into Graph via the Content Source API. The external system owns every field; the CMS surfaces it as a read-only connected type that editors can browse and reference but cannot edit.

  • Source system owns every field
  • Not editable in the CMS
  • Browse and reference via Connect from Graph
  • No draft/publish, preview, or FX targeting
  • Re-sync from the source to update
Use only when the source of truth is another system - CRM, PIM, commerce, or DAM. This is the path the rest of this page documents.
?

Rule of thumb - pick by where the source of truth lives

If editors need to create or change the content, or it has no home outside Optimizely, push it into the CMS. Reach for the Content Source API only when a separate system already owns the data and the CMS just needs to reference it. For the full capability breakdown, see CMS-native vs External Content ↓.

Live Example - Quotes #

Each card is a Quote item synced from an external source via the _Item base type. Custom properties (author, text) hold application data and are queried directly.

Sarah Chen

I moved my savings to Mosey after seeing their 5.1% AER rate. The transfer took less than a day and the app makes it easy to watch my interest grow.

Marcus Webb

Applied for a mortgage online on a Sunday. Had a decision in principle by Monday morning. The advisor called to walk me through the full offer — never felt rushed.

Aisha Okafor

The mobile app notifications are brilliant. I know exactly where my money is going and the spending insights helped me save an extra £200 last month.

Tom Hartley

Opened a business current account in under 15 minutes. The integration with our accounting software was seamless — invoices reconcile automatically.

Priya Sharma

I had a fraud alert on my card at 2am. I called the number and got through to a real person in under a minute. Card blocked, new one dispatched, sorted.

Daniel Reeves

Switched from my old bank after 12 years. Mosey's CASS switch took 7 working days and every direct debit moved without any issues whatsoever.

Querying External Content #

Once synced, external content is queried exactly like CMS-managed content - same GraphQL endpoint, same ISR caching. Query your custom properties directly; _itemMetadata fields are search-indexed internals and return null at query time. SDK docs ↗

Quotes

query GetQuotes {
    Quote(limit: 100, orderBy: { author: { value: ASC } }) {
      items {
        author
        text
      }
    }
  }

Bank Locations

query GetBankLocations {
    BankLocation(limit: 20, orderBy: { city: ASC }) {
      items {
        
  branchName
  city
  country
  phone
  services
  location {
    lat
    lon
  }

      }
    }
  }

Sync Paths - Getting Data into Graph #

Three paths push external data straight into Optimizely Graph. All three end up in the same place - data queryable via GraphQL alongside CMS content - but differ in who owns the pipeline, whether scheduling is managed, and what third-party tooling is involved. When the content is something the CMS itself should own, use the fourth method - Direct to CMS - instead: you author it in the CMS and it is published to Graph for you, fully editable.

Connecting to CMS - same step for every external path

Once data is in Graph, wire it to CMS via Admin → Content Types → Create new… → Connect from Graph. Choose the source ID, schema, a CMS base type (Page, Component, Media, Image, or Video), and the fields to use as the content ID and display name. CMS creates a read-only connected content type editors can reference and browse in the Content Manager. Note: the label is Connect from Graph, not "Import from Graph" - a common source of confusion in the UI. The Direct to CMS method below skips this entirely - that content is already native CMS content.

1

Direct to Graph

Content Source API - NdJSON over HTTP

Register a schema via PUT /api/content/v3/types and push NdJSON records via POST /api/content/v2/data. No third-party tools required. You build and run all sync logic.

SchemaRegistered via API
DataPushed directly to Graph
SyncYour code, your schedule
Infra you ownEverything
Best for: Custom or proprietary data with no off-the-shelf integration. Teams comfortable owning scheduling, retries, and rebuilds.

The payload format has several undocumented requirements (schema flags, typed field names, metadata objects, _rbac) - see Base Type Contracts ↓ for the full rules and working examples.

Additional sources may need to be enabled by Optimizely - contact support if the types endpoint returns a source-limit error.

2

OCP - Free Tier

Public apps + Custom Endpoints (real-time)

Included with SaaS CMS. Ships with public apps for common platforms - each app handles auth, schema registration, and field mapping automatically. If no public app exists, Custom Endpoints (real-time) adds a field-mapping UI over what you'd build for Path 1.

SchemaOCP config + Graph
DataWebhook ingestion
SyncReal-time only (free tier)
Infra you ownYour data pipeline

Public apps (free)

BynderBrandfolderCommercetoolsShopifyWordPress
Best for: Customers using a supported public app - this is the unambiguous recommendation for Bynder, Brandfolder, or Commercetools. For custom data with no matching app, the free tier adds little over Path 1 and doesn't support scheduled syncs from your source.
3

OCP - Paid Tier

Managed staging DB + scheduled syncs

Unlocks the OCP database as a hosted staging layer and scheduled outbound syncs to Graph via the Sync Manager. Data reaches OCP via S3 CSV, REST API, webhook, or an OCP app - from there OCP handles the sync to Graph on your chosen interval.

SchemaOCP database + Graph
DataS3 / API / app → OCP DB → Graph
SyncScheduled or real-time (OCP managed)
Infra you ownLess - OCP hosts staging + intervals
Best for: Customers who want OCP to manage the staging layer and scheduling without building their own pipeline. Also the right path if data needs to flow from one source to multiple OCP destinations.

Paid tier required - confirm pricing with the CSM before scoping.

Reference OCP app: joshuaonwezen/ocp-product-catalog - TypeScript app with KV store, REST API, and bulk + real-time Graph sync.

Direct to CMS

Management API - editable CMS-native content

Skip Graph and push into the CMS itself. Create the item with POST /v1/content on the Management API (field values wrapped as PropertyData), then publish it via the versions publish endpoint - or author it directly in Visual Builder. The CMS owns the item and publishes it into Graph for you, so it lands on the same GraphQL endpoint as everything else.

SchemaCMS content types
DataAuthored in the CMS
SyncCMS publishes to Graph
Infra you ownNone - CMS-managed
Best for: Content the CMS should own - editors create and edit it, and it needs a draft/publish lifecycle, preview, or FX targeting. Unlike the paths above, this content is fully editable and never goes through Connect from Graph.

See Management API ↗ for the full content-creation workflow - endpoints, payload shape, and publishing.

Recommendation Matrix

Custom data, full control, no managed layer needed

Path 1

Using Bynder, Brandfolder, Commercetools, Shopify, or WordPress

Public app handles auth, schema, and field mapping

Path 2

Want managed scheduling + staging layer, paid tier acceptable

Confirm pricing with CSM before scoping

Path 3

Free OCP tier + scheduled (not real-time) syncs from source

Free tier only supports real-time push - scheduled source syncs are paid-only

Path 1 or 3

Content the CMS should own and editors need to edit

Authored via the Management API or Visual Builder - editable, not read-only

Direct to CMS

Base Type Contracts #

Graph ships three built-in base type contracts. Inherit from one when registering a content type - it adds the metadata property Graph needs to identify, index, and surface your items. All registrations require "preset": "next" and "useTypedFieldNames": true.

When pushing data, the displayName field inside any metadata object must be written as displayName___searchable. This is because displayName is marked searchable: true in the contract definition - with useTypedFieldNames enabled, Graph appends ___searchable to distinguish full-text indexed fields from plain stored fields in the payload. Fields in _assetMetadata and _imageMetadata are not searchable and keep their original names.

Three additional payload rules apply to every push. First, _rbac must be the string "r:Everyone:Read" - sending an object here (e.g. { read: ["Everyone"] }) triggers an Elasticsearch mapper_parsing_exception that corrupts the index and silently drops every record; the only fix is to delete the entire source and re-register. Second, custom field names must include a $$Type suffix matching their schema type: field$$String, field$$Float, field$$Boolean, etc. - plain field names are accepted but not indexed. Third, a top-level _metadata object is required alongside _itemMetadata, carrying types, locale, key, and status. All three rules are absent from the official docs; the examples below reflect the working format confirmed with Optimizely engineers.

_Item

→ adds _itemMetadata

The base contract for all external items. Use this for structured data without a file attachment - testimonials, product catalog entries, CRM records, quotes.

Type Registration

PUT https://cg.optimizely.com/api/content/v3/types?id=quot
Content-Type: application/json
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{
  "contentTypes": {
    "Quote": {
      "contentType": ["_Item"],
      "properties": {
        "author": { "type": "String" },
        "text":   { "type": "String" }
      }
    }
  },
  "preset": "next",
  "useTypedFieldNames": true
}

Data Payload (NdJSON)

POST https://cg.optimizely.com/api/content/v2/data?id=quot
Content-Type: text/plain
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{"index": {"_id": 1, "language_routing": "en"}}
{
  "_rbac": "r:Everyone:Read",
  "_itemMetadata": {
    "key": "qt-1",
    "displayName___searchable": "Quote - Sarah Chen",
    "lastModified": "2026-06-07T00:00:00.000Z",
    "type": "Quote"
  },
  "_metadata": {
    "types": ["Quote", "_Item"],
    "locale": "en",
    "key": "qt-1",
    "status": "Published"
  },
  "author$$String": "Sarah Chen",
  "text$$String": "I moved my savings to Mosey after seeing their 5.1% AER rate...",
  "ContentType": ["Quote"],
  "Status": "Published",
  "Language": { "DisplayName": "English", "Name": "en" }
}

_AssetItem

→ adds _itemMetadata + _assetMetadata

Extends _Item with _assetMetadata - adds fileSize, mimeType, and url. Use for PDFs, videos, audio, or any binary-backed asset from a DAM or CDN.

Type Registration

PUT https://cg.optimizely.com/api/content/v3/types?id=docs
Content-Type: application/json
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{
  "contentTypes": {
    "Document": {
      "contentType": ["_AssetItem"],
      "properties": {
        "title":       { "type": "String" },
        "description": { "type": "String" }
      }
    }
  },
  "preset": "next",
  "useTypedFieldNames": true
}

Data Payload (NdJSON)

POST https://cg.optimizely.com/api/content/v2/data?id=docs
Content-Type: text/plain
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{"index": {"_id": 1, "language_routing": "en"}}
{
  "_rbac": "r:Everyone:Read",
  "_itemMetadata": {
    "key": "doc-1",
    "displayName___searchable": "Product Datasheet",
    "lastModified": "2026-05-26T00:00:00.000Z",
    "type": "Document"
  },
  "_metadata": {
    "types": ["Document", "_AssetItem", "_Item"],
    "locale": "en",
    "key": "doc-1",
    "status": "Published"
  },
  "_assetMetadata": {
    "fileSize": 245760,
    "mimeType": "application/pdf",
    "url": "https://example.com/docs/product-datasheet.pdf"
  },
  "title$$String": "Product Datasheet",
  "description$$String": "Full technical specifications for the Enterprise plan.",
  "ContentType": ["Document"],
  "Status": "Published",
  "Language": { "DisplayName": "English", "Name": "en" }
}

_ImageItem

→ adds _itemMetadata + _assetMetadata + _imageMetadata

Extends _AssetItem with _imageMetadata - adds width and height. Use for images from a DAM or media library where you need dimensions queryable at render time - for example to compute aspect ratios or avoid layout shift. All three metadata objects are required in the payload.

Type Registration

PUT https://cg.optimizely.com/api/content/v3/types?id=photos
Content-Type: application/json
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{
  "contentTypes": {
    "Photo": {
      "contentType": ["_ImageItem"],
      "properties": {
        "altText": { "type": "String" },
        "caption": { "type": "String" }
      }
    }
  },
  "preset": "next",
  "useTypedFieldNames": true
}

Data Payload (NdJSON)

POST https://cg.optimizely.com/api/content/v2/data?id=photos
Content-Type: text/plain
Authorization: Basic <base64(APP_KEY:APP_SECRET)>

{"index": {"_id": 1, "language_routing": "en"}}
{
  "_rbac": "r:Everyone:Read",
  "_itemMetadata": {
    "key": "photo-1",
    "displayName___searchable": "Product Hero Image",
    "lastModified": "2026-05-26T00:00:00.000Z",
    "type": "Photo"
  },
  "_metadata": {
    "types": ["Photo", "_ImageItem", "_AssetItem", "_Item"],
    "locale": "en",
    "key": "photo-1",
    "status": "Published"
  },
  "_assetMetadata": {
    "fileSize": 1048576,
    "mimeType": "image/jpeg",
    "url": "https://example.com/images/product-hero.jpg"
  },
  "_imageMetadata": {
    "width": 1920,
    "height": 1080
  },
  "altText$$String": "Optimizely platform dashboard screenshot",
  "caption$$String": "The Visual Builder interface showing a live page edit.",
  "ContentType": ["Photo"],
  "Status": "Published",
  "Language": { "DisplayName": "English", "Name": "en" }
}

CMS-native vs External Content #

Data pushed via the Content Source API lands in Graph as read-only external content. Editors can browse and reference it in the CMS via Admin - Content Types - Connect from Graph, but they cannot edit the fields - the source system owns the data, and the only way to change it is to re-sync from that system.

CMS-native content is the editable alternative. Instead of pushing into Graph, you push directly into the CMS - either through the Management API (POST /v1/content, as the seed scripts do) or by authoring in Visual Builder. The result is a real CMS content item, not a connected external type: it carries a draft - review - publish lifecycle, is editable field-by-field in the Content Manager, works in preview mode, can be targeted by FX experiments through CMS Variations, and holds per-locale values on isLocalized fields. It is still queryable through the same Graph endpoint - the CMS publishes it into Graph for you. The live grids below contrast both: external quotes pushed via the Content Source API against CMS-native QuoteBlock items you can open and edit in the CMS.

Capability comparison

CapabilityExternal (Content Source API)CMS-native (Management API)
Edit fields in CMSNo - read-only connected typeYes - full Visual Builder editing
Draft / publish lifecycleNo - status set at sync timeYes - draft, review, publish
Preview modeNoYes - CMS inline edit + preview
FX experiment targetingNoYes - CMS Variations per flag key
LocalizationPer-locale sync recordsYes - isLocalized fields per locale
Webhook revalidationManual revalidate tag callYes - publish webhook fires automatically
Queryable in GraphYes - same GraphQL endpointYes - same GraphQL endpoint
Source of truthExternal system (CRM, PIM, etc.)Optimizely CMS

External quotes

demo data

Pushed via POST /api/content/v2/data. Not editable in the CMS - the seed script owns every field.

I moved my savings to Mosey after seeing their 5.1% AER rate. The transfer took less than a day and the app makes it easy to watch my interest grow.

Sarah Chen

Applied for a mortgage online on a Sunday. Had a decision in principle by Monday morning. The advisor called to walk me through the full offer — never felt rushed.

Marcus Webb

The mobile app notifications are brilliant. I know exactly where my money is going and the spending insights helped me save an extra £200 last month.

Aisha Okafor

CMS-native quotes

run seed-quote-blocks.ts

Created as QuoteBlock shared blocks via the Management API. Open any in the CMS to edit author, role, and quote text.

No CMS-native quotes found in Graph yet.

  1. 1. Run npm run opti:push to register QuoteBlock
  2. 2. Run npx tsx scripts/seed-quote-blocks.ts
  3. 3. Wait ~60s for Graph to index, then reload

Graph query - CMS-native QuoteBlock

query GetQuoteBlocks {
    QuoteBlock(limit: 100, orderBy: { author: { value: ASC } }) {
      items {
        author
        role
        text
      }
    }
  }