Developer Demo
External Content Sync
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
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
Push into Graph
Content Source API - this page
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
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.
“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.”
“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.”
“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.”
“Opened a business current account in under 15 minutes. The integration with our accounting software was seamless — invoices reconcile automatically.”
“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.”
“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.
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.
⚠ 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.
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.
Public apps (free)
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.
⚠ 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.
See Management API ↗ for the full content-creation workflow - endpoints, payload shape, and publishing.
Recommendation Matrix
Custom data, full control, no managed layer needed
Using Bynder, Brandfolder, Commercetools, Shopify, or WordPress
Public app handles auth, schema, and field mapping
Want managed scheduling + staging layer, paid tier acceptable
Confirm pricing with CSM before scoping
Free OCP tier + scheduled (not real-time) syncs from source
Free tier only supports real-time push - scheduled source syncs are paid-only
Content the CMS should own and editors need to edit
Authored via the Management API or Visual Builder - editable, not read-only
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 _itemMetadataThe 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 + _assetMetadataExtends _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 + _imageMetadataExtends _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
| Capability | External (Content Source API) | CMS-native (Management API) |
|---|---|---|
| Edit fields in CMS | No - read-only connected type | Yes - full Visual Builder editing |
| Draft / publish lifecycle | No - status set at sync time | Yes - draft, review, publish |
| Preview mode | No | Yes - CMS inline edit + preview |
| FX experiment targeting | No | Yes - CMS Variations per flag key |
| Localization | Per-locale sync records | Yes - isLocalized fields per locale |
| Webhook revalidation | Manual revalidate tag call | Yes - publish webhook fires automatically |
| Queryable in Graph | Yes - same GraphQL endpoint | Yes - same GraphQL endpoint |
| Source of truth | External system (CRM, PIM, etc.) | Optimizely CMS |
External quotes
demo dataPushed 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.tsCreated 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. Run
npm run opti:pushto registerQuoteBlock - 2. Run
npx tsx scripts/seed-quote-blocks.ts - 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
}
}
}