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.
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 exist for pushing external data 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.
Connecting to CMS - same step for every 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.
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.
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
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" }
}