How to Integrate Ninetailed with Your Headless CMS
Connect Ninetailed to your headless CMS so marketers can publish personalized variants and A/B tests from structured content without asking developers to hard-code every audience rule.
What is Ninetailed?
Ninetailed is a personalization and experimentation platform for composable digital teams. It lets teams define audiences, run A/B tests, personalize content variants, and measure performance across web experiences. Marketing, growth, and product teams use Ninetailed when they need personalization logic to work with their existing content, analytics, and frontend stack.
Why integrate Ninetailed with a headless CMS?
Personalization breaks down when content and audience logic live in separate places. A marketer publishes a campaign hero in one tool, configures an audience in another, asks a developer to connect them, and waits for the next deploy. That slows down simple changes, like showing different pricing-page messaging to first-time visitors, returning customers, and enterprise accounts.,Connecting Ninetailed to a headless CMS solves that handoff. Your content team can model personalized experiences as structured fields, including a default experience, variant entries, audience IDs, experiment names, and fallback rules. Ninetailed handles audience matching and experiment delivery, while the structured content source provides the exact copy, images, CTAs, and product references that each variant needs.,With Sanity, the integration can react the moment content changes. A publish event can trigger a webhook or Function, GROQ can fetch only the fields Ninetailed needs, and the sync can update Ninetailed without polling or nightly batch jobs. The trade-off is that you need to design the content model carefully. If every page has a different variant shape, your integration gets harder to test and harder for marketers to reuse.
Architecture overview
A typical Sanity and Ninetailed integration starts with structured experience documents in the Content Lake. For example, a landing page can reference a default hero and an array of personalized variants, each mapped to a Ninetailed audience or experiment ID. When an editor publishes that page in Sanity Studio, a webhook fires on the publish mutation, or a Sanity Function runs server-side. The handler receives the document ID, uses @sanity/client and GROQ to fetch the latest published fields, including referenced assets and variant content, then calls Ninetailed's Management API to create or update the corresponding experience configuration. On the frontend, your Next.js, Nuxt, or other app fetches page content from Sanity, initializes Ninetailed's SDK with the site client ID and environment, and renders the right variant for the visitor based on Ninetailed profile, audience, and experiment decisions. Sanity remains the structured source for content, Ninetailed makes the personalization decision, and the user sees the correct variant at request time or in the browser, depending on your rendering setup.
Common use cases
Audience-based landing pages
Show different headlines, proof points, and CTAs to visitors from paid search, existing customers, or target accounts using Ninetailed audiences tied to Sanity variants.
A/B test campaign content
Run experiments on hero copy, pricing-page CTAs, form layouts, or promotional banners while keeping every test variant editable in Sanity Studio.
Personalized commerce merchandising
Use Ninetailed profile data to choose Sanity-authored product modules, bundles, or recommendations for new visitors, loyal customers, and high-intent shoppers.
Regional content targeting
Serve region-specific offers, legal copy, language variants, or event promotions by connecting Ninetailed audience rules to structured Sanity content.
Step-by-step integration
- 1
Set up Ninetailed
Create a Ninetailed account, add your site as a property, choose the environment you want to use, and copy the client ID plus any Management API credentials from your Ninetailed workspace. Install the frontend package that matches your stack, such as @ninetailed/experience.js-next for Next.js or @ninetailed/experience.js-react for React.
- 2
Model personalization fields in Sanity Studio
Create schema fields for the default experience, variant content, Ninetailed audience ID, experiment ID, traffic split, and fallback behavior. Keep the variant shape consistent. For example, if a hero has headline, image, CTA label, and CTA URL, every variant should use the same fields.
- 3
Create a publish trigger
Use a Sanity webhook filtered to published experience documents, or use a Sanity Function if you want the sync logic to run inside Sanity without hosting a separate listener. Include the document ID and dataset in the webhook payload.
- 4
Fetch the exact content with GROQ
In the handler, use @sanity/client to fetch the published document and resolve referenced content, images, and linked CTAs. GROQ lets you return only the fields Ninetailed needs, which keeps payloads small and avoids sending draft-only editorial fields.
- 5
Sync to Ninetailed
Map Sanity fields to Ninetailed experience, audience, and variant fields, then call Ninetailed's Management API with your API key. Keep a ninetailedExperienceId on the Sanity document so later publishes update the same experience instead of creating duplicates.
- 6
Test the frontend experience
Initialize Ninetailed's SDK in your app, fetch the page content from Sanity, and confirm that each audience receives the expected variant. Test at least three paths: default visitor, matched audience visitor, and experiment fallback when Ninetailed is unavailable.
Code example
Minimal webhook handler that receives a Sanity publish event, fetches an experience document with GROQ, and upserts it to Ninetailed's Management API.
import {createClient} from '@sanity/client'
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-01-01',
token: process.env.SANITY_READ_TOKEN!,
useCdn: false,
})
export async function POST(req: Request) {
const {documentId} = await req.json()
const experience = await sanity.fetch(`
*[_id == $id][0]{
_id,
title,
ninetailedExperienceId,
defaultHero{headline, ctaLabel, ctaUrl},
variants[]{
audienceId,
weight,
hero{headline, ctaLabel, ctaUrl}
}
}
`, {id: documentId})
const res = await fetch(
`https://api.ninetailed.co/v2/organizations/${process.env.NINETAILED_ORG_ID}/environments/${process.env.NINETAILED_ENV}/experiences/${experience.ninetailedExperienceId}`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.NINETAILED_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: experience.title,
baseline: experience.defaultHero,
variants: experience.variants.map((v: any) => ({
audienceId: v.audienceId,
weight: v.weight,
content: v.hero,
})),
}),
}
)
if (!res.ok) throw new Error(await res.text())
return Response.json({ok: true})
}How Sanity + Ninetailed works
Build your Ninetailed integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs to connect Ninetailed personalization and experimentation to every content experience.
Start building free โCMS approaches to Ninetailed
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Personalized content modeling | Personalization fields often live inside page templates or plugins, which can make variants hard to reuse across channels. | Schema-as-code lets you model default content, variants, audience IDs, experiment IDs, and fallback rules as typed fields in Sanity Studio. |
| Sync on publish | Teams often rely on plugin-specific sync behavior or manual republishing when personalized content changes. | Webhooks can trigger on publish, and Functions can run server-side sync logic without a separate service. |
| Field-level control for Ninetailed payloads | Rendered HTML or template data may include more than Ninetailed needs, which increases mapping work. | GROQ can fetch exactly the fields Ninetailed needs, including joined references, in one query. |
| Experiment variant governance | Editors may create test copy directly inside pages, which can make it hard to review active variants across the site. | Sanity Studio can show active experiments, variant status, approval fields, Comments, Tasks, and Content Releases in one editorial workflow. |
| Multi-channel personalization | Personalization is usually tied to the website rendering layer. | The same structured content can feed web, mobile, Ninetailed, and AI agents, with channel-specific projections through GROQ. |
Keep building
Explore related integrations to complete your content stack.
Sanity + LaunchDarkly
Control feature flags, staged rollouts, and content-dependent releases using structured Sanity content and LaunchDarkly targeting.
Sanity + Optimizely
Connect structured content to Optimizely experiments so teams can test page modules, offers, and conversion flows.
Sanity + Uniform
Use Sanity content with Uniform composition and personalization logic for teams building multi-source digital experiences.