Personalization & Experimentation8 min read

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.

Published April 29, 2026
01 โ€” Overview

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.


02 โ€” The case for integration

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.


03 โ€” Architecture

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.


04 โ€” Use cases

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.


05 โ€” Implementation

Step-by-step integration

  1. 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. 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. 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. 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. 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. 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.


06 โ€” Code

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.

typescript
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})
}

07 โ€” Why Sanity

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 โ†’

08 โ€” Comparison

CMS approaches to Ninetailed

CapabilityTraditional CMSSanity
Personalized content modelingPersonalization 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 publishTeams 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 payloadsRendered 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 governanceEditors 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 personalizationPersonalization 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.

09 โ€” Next steps

Keep building

Explore related integrations to complete your content stack.

Ready to try Sanity?

See how Sanity's Content Operating System powers integrations with Ninetailed and 200+ other tools.