Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change Site Theme
Sanity logo

Documentation

    • Overview
    • Platform introduction
    • Next.js quickstart
    • Nuxt.js quickstart
    • Astro quickstart
    • React Router quickstart
    • Studio quickstart
    • Build with AI
    • Content Lake
    • Functions
    • APIs and SDKs
    • Visual Editing
    • Blueprints
    • Platform management
    • Dashboard
    • Studio
    • Canvas
    • Media Library
    • App SDK
    • Content Agent
    • HTTP API
    • CLI
    • Libraries
    • Specifications
    • Changelog
    • User guides
    • Developer guides
    • Courses and certifications
    • Join the community
    • Templates
Developer guides
Overview

  • Develop with AI

    Best practices

  • Query optimization

    Paginating with GROQ
    High performance GROQ

  • Roles and authentication

    Setting up Single Sign-On with SAML
    Third-Party Login (Deprecated)
    OAuth2
    Set up SSO authentication with SAML and Azure/Entra ID
    Set up SSO authentication with SAML and PingIdentity
    Set up SSO authentication with SAML and JumpCloud
    Reconcile users against internal systems
    Restrict Access to Specific Documents
    Setting up a Default Relay State for IdP Initiated - SAML Logins

  • Structured content

    Scalable navigation patterns
    An opinionated guide to Sanity Studio
    Browsing Content How You Want with Structure Builder
    Deciding on fields and relationships
    Create richer array item previews
    Dynamic folder structure using the currentUser and workflow states
    Create a time duration object field
    Level up Your Edit Modal with Next/Previous Navigation Buttons for Array Items
    Create a “coupon generator” string field input
    Managing redirects with Sanity
    Create a document form progress component
    Create an array input field with selectable templates
    Creating a Parent/Child Taxonomy
    Create interactive array items for featured elements
    Create a visual string selector field input
    Create a survey rating number field input
    How to use structured content for page building
    Create a recycling bin for deleted documents via Sanity Functions

  • Frontend integration

    Add live content to your application
    Forms with Sanity
    Vercel Integration
    Build your blog with Astro and Sanity
    How to implement front-end search with Sanity

  • Ecommerce

    Displaying Sanity content in Shopify
    Sanity Connect for Shopify
    Custom sync handlers for Sanity Connect

  • Integrating with other services

    A/B testing with Sanity and Growthbook
    Cookie consent integrations with Sanity
    Integrating external data sources with Sanity
    Klaviyo (email campaigns)
    Developing with Next.js on GitHub Codespaces

  • Adopting Sanity

    How to pitch Sanity.io to your team
    Convincing your clients to go with Sanity.io, rather than a traditional CMS
    Not-profit plan
    Agencies: Navigating the Spring 2025 Organization Changes
    How to generate massive amounts of demo content for Sanity
    How to implement Multi-tenancy with Sanity

  • GROQ

    GROQ-Powered Webhooks – Intro to Filters
    GROQ-Powered Webhooks – Intro to Projections

  • Portable Text

    Presenting Portable Text
    Add Inline blocks for the Portable Text Editor
    Beginners guide to Portable Text
    How to add custom YouTube blocks to Portable Text
    Converting Inline Styles to Sanity Block Decorators
    Add things to Portable Text
    Change the height of the PTE

  • Community and ecosystem

    Create your own Sanity template
    Community guides
    Community Code of Conduct
    Contribute to the ecosystem

  • Plugin development

    Migrating plugins to support Content Releases

On this page

Previous

Cookie consent integrations with Sanity

Next

Klaviyo (email campaigns)

Was this page helpful?

On this page

  • 2 ways to integrate
  • Creating a document for each item (sync plugin)
  • Overview
  • Pros and cons
  • Example
  • Saving as fields (input plugin)
  • Overview
  • Pros and cons
  • Example
Developer guidesLast updated September 24, 2025

Integrating external data sources with Sanity

Learn the 2 most common patterns for integrating external data sources with Sanity

This developer guide was contributed by Chris LaRocque (Senior Solution Architect).

This guide will explain the 2 common patterns for integrating external data sources with Sanity. Our plugins page includes pre-built integrations for popular platforms, but if an integration doesn’t exist this guide can help walk you through how to build one of your own.

2 ways to integrate

There are 2 primary ways to bring external data into Sanity:

  • Creating documents for each external “item” (commonly referred to in Sanity terms as a sync plugin), or…
  • Saving an “item” as a field’s value on an as-needed basis (often referred to as an input plugin)

Creating a document for each item (sync plugin)

Overview

Creating a document for each item in an external system is often referred to as a sync plugin in Sanity terminology. The best example of a Sanity sync plugin would be Sanity Connect for Shopify, which has excellent documentation showing custom handlers that illustrate the approximate process a sync plugin uses to keep Sanity up to date with external data:

  • A sync is triggered - this could be from a webhook (if the external system supports them) or something less granular like a cron job
  • Determine the data to be synced - Most webhooks will provide exactly what changed, but some cases may require comparing updated timestamps between the external data and Sanity documents
  • Create or update the relevant Sanity documents - Use our client or our Actions API to create or update the relevant documents

Pros and cons

Pros

  • Data for the front-end can all be fetched from Sanity’s API in 1 query, as opposed to one call to Sanity and a 2nd call to the external service
  • Can be expanded to allow 2-way syncing, where changes in Sanity are “pushed” back to the external system (via GROQ webhooks)
  • Studio users can see all data for each item

Cons

  • More infrastructure usage required for sync process - usually a serverless function to run the sync and a cron job or webhook to trigger the syncs
  • Typically a more involved development task than adding data to a field
  • Depending on external system’s capabilities, changes may not be synced with Sanity immediately
  • The schema for the external items used by the Sanity Studio must be kept up to date with the external data

Example

The following is a simplified breakdown of the code example shown in the Shopify Sanity Connect docs. It shows a serverless function that receives a webhook from Shopify when products are created, updated, or deleted, and syncs those changes to documents in Sanity.

// ./src/pages/api/sync-handler.ts
import { createClient } from "@sanity/client";

// Create a Sanity client with a write token to allow creating and updating of documents
// Read more on auth, tokens and securing them: https://www.sanity.io/docs/http-auth
const sanityClient = createClient({
  apiVersion: "2025-02-04",
  dataset: process.env.SANITY_DATASET,
  projectId: process.env.SANITY_PROJECT_ID,
  token: process.env.SANITY_ADMIN_AUTH_TOKEN,
  useCdn: false,
});

/**
 * A Next.js API route handler for the pages router
 * Takes incoming webhooks and creates/updates/deletes documents based on external system's changes
 */
export default async function handler(req, res) {
  const { body } = req;

  try {
    // Create a transaction to batch operations to Sanity
    const transaction = sanityClient.transaction();

    // Perform different operations based on the webhook action type
    switch (body.action) {
      case "create":
      case "update":
      case "sync":
        await createOrUpdateProducts(transaction, body.products);
        break;
      case "delete":
        const documentIds = body.productIds.map((id) =>
          getDocumentProductId(id)
        );
        await deleteProducts(transaction, documentIds);
        break;
    }
    await transaction.commit();
  } catch (err) {
    console.error("Transaction failed: ", err.message);
  }

  res.status(200).json({ message: "OK" });
}

/**
 * Creates (or updates if already existing) Sanity documents of type `shopify.product`.
 * Patches existing drafts too, if present.
 *
 * All products will be created with a deterministic _id in the format `product-${SHOPIFY_ID}`
 */
async function createOrUpdateProducts(transaction, products) {
  // Extract draft document IDs from current update
  const draftDocumentIds = products.map((product) => {
    const productId = extractIdFromGid(product.id);
    return `drafts.${getDocumentProductId(productId)}`;
  });

  // Determine if drafts exist for any updated products
  const existingDrafts = await sanityClient.fetch(`*[_id in $ids]._id`, {
    ids: draftDocumentIds,
  });

  products.forEach((product) => {
    // Build Sanity product document
    const document = buildProductDocument(product);
    const draftId = `drafts.${document._id}`;

    // Create (or update) existing published document
    transaction
      .createIfNotExists(document)
      .patch(document._id, (patch) => patch.set(document));

    // Check if this product has a corresponding draft and if so, update that too.
    if (existingDrafts.includes(draftId)) {
      transaction.patch(draftId, (patch) =>
        patch.set({
          ...document,
          _id: draftId,
        })
      );
    }
  });
}

/**
 * Delete corresponding Sanity documents of type `shopify.product`.
 * Published and draft documents will be deleted.
 */
async function deleteProducts(transaction, documentIds) {
  documentIds.forEach((id) => {
    transaction.delete(id).delete(`drafts.${id}`);
  });
}

/**
 * Build Sanity document from webhook product payload
 */
function buildProductDocument(product) {
  const {
    featuredImage,
    id,
    productType,
    priceRange,
    status,
    title,
    productId
  } = product;

  // Build Sanity document 
  return {
    _id: getDocumentProductId(productId),
    _type: "shopify.product",
    image: featuredImage?.src,
    priceRange,
    productType,
    status,
    title,
  };
}

/**
 * Map Shopify product ID number to a corresponding Sanity document ID string
 * e.g. 12345 => product-12345
 */
function getDocumentProductId(productId) {
  return `product-${productId}`;
}

Again, this is simplified to illustrate a typical workflow, check out the example in the Sanity Connect docs for a better real world example, including things like better error handling that were removed here for brevity.

Saving as fields (input plugin)

Overview

Integrating external data as field values is typically referred to as an input plugin. Input plugins will provide a custom field type that includes an input for browsing the data in the external system from the Studio, where selecting an item sets the field’s value. The data saved to the field can vary in complexity to match your use case, some plugins will save just a string for an item’s name or ID, others will copy an object with several properties, like a set of URLs for different image formats.

@sanity/sanity-plugin-async-list provides an easy starting point for this type of implementation

Pros and cons

Pros

  • Less time to implement
  • Overall simpler integration - less points of failure and less potential ongoing maintenance
  • Allows external system to be the source of truth - can be beneficial if the external data is frequently changing

Cons

  • If the data changes in the external system, the data in Sanity will not be updated automatically and will be outdated until a content migration is ran. For this reason it’s best to sync fields that are considered immutable in the external system, like an id.

Example

Here’s an example using @sanity/sanity-plugin-async-list to fetch the names of Disney Characters and add them as an input in a document.

// sanity.config.ts
import {defineConfig} from 'sanity'
import {asyncList} from '@sanity/sanity-plugin-async-list'

export default defineConfig({
	// ...rest of config
  plugins: [
    asyncList({
      schemaType: 'disneyCharacter',
      loader: async () => {
        const response = await fetch('https://api.disneyapi.dev/character')
        const result: {data: {name: string}[]} = await response.json()

        return result.data.map((item) => {
          return {value: item.name, ...item}
        })
      },
    }),
   // ...rest of plugins
  ],
})

Add the name from schemaType to the document type where you want to use the field

// post.ts
import {defineField, defineType} from 'sanity'

export default defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({
      name: 'disney',
      type: 'disneyCharacter',
    }),
  ],
})

Then see the field in your Studio

Loading...
The field in your Sanity document fetching remote data
// ./src/pages/api/sync-handler.ts
import { createClient } from "@sanity/client";

// Create a Sanity client with a write token to allow creating and updating of documents
// Read more on auth, tokens and securing them: https://www.sanity.io/docs/http-auth
const sanityClient = createClient({
  apiVersion: "2025-02-04",
  dataset: process.env.SANITY_DATASET,
  projectId: process.env.SANITY_PROJECT_ID,
  token: process.env.SANITY_ADMIN_AUTH_TOKEN,
  useCdn: false,
});

/**
 * A Next.js API route handler for the pages router
 * Takes incoming webhooks and creates/updates/deletes documents based on external system's changes
 */
export default async function handler(req, res) {
  const { body } = req;

  try {
    // Create a transaction to batch operations to Sanity
    const transaction = sanityClient.transaction();

    // Perform different operations based on the webhook action type
    switch (body.action) {
      case "create":
      case "update":
      case "sync":
        await createOrUpdateProducts(transaction, body.products);
        break;
      case "delete":
        const documentIds = body.productIds.map((id) =>
          getDocumentProductId(id)
        );
        await deleteProducts(transaction, documentIds);
        break;
    }
    await transaction.commit();
  } catch (err) {
    console.error("Transaction failed: ", err.message);
  }

  res.status(200).json({ message: "OK" });
}

/**
 * Creates (or updates if already existing) Sanity documents of type `shopify.product`.
 * Patches existing drafts too, if present.
 *
 * All products will be created with a deterministic _id in the format `product-${SHOPIFY_ID}`
 */
async function createOrUpdateProducts(transaction, products) {
  // Extract draft document IDs from current update
  const draftDocumentIds = products.map((product) => {
    const productId = extractIdFromGid(product.id);
    return `drafts.${getDocumentProductId(productId)}`;
  });

  // Determine if drafts exist for any updated products
  const existingDrafts = await sanityClient.fetch(`*[_id in $ids]._id`, {
    ids: draftDocumentIds,
  });

  products.forEach((product) => {
    // Build Sanity product document
    const document = buildProductDocument(product);
    const draftId = `drafts.${document._id}`;

    // Create (or update) existing published document
    transaction
      .createIfNotExists(document)
      .patch(document._id, (patch) => patch.set(document));

    // Check if this product has a corresponding draft and if so, update that too.
    if (existingDrafts.includes(draftId)) {
      transaction.patch(draftId, (patch) =>
        patch.set({
          ...document,
          _id: draftId,
        })
      );
    }
  });
}

/**
 * Delete corresponding Sanity documents of type `shopify.product`.
 * Published and draft documents will be deleted.
 */
async function deleteProducts(transaction, documentIds) {
  documentIds.forEach((id) => {
    transaction.delete(id).delete(`drafts.${id}`);
  });
}

/**
 * Build Sanity document from webhook product payload
 */
function buildProductDocument(product) {
  const {
    featuredImage,
    id,
    productType,
    priceRange,
    status,
    title,
    productId
  } = product;

  // Build Sanity document 
  return {
    _id: getDocumentProductId(productId),
    _type: "shopify.product",
    image: featuredImage?.src,
    priceRange,
    productType,
    status,
    title,
  };
}

/**
 * Map Shopify product ID number to a corresponding Sanity document ID string
 * e.g. 12345 => product-12345
 */
function getDocumentProductId(productId) {
  return `product-${productId}`;
}
// sanity.config.ts
import {defineConfig} from 'sanity'
import {asyncList} from '@sanity/sanity-plugin-async-list'

export default defineConfig({
	// ...rest of config
  plugins: [
    asyncList({
      schemaType: 'disneyCharacter',
      loader: async () => {
        const response = await fetch('https://api.disneyapi.dev/character')
        const result: {data: {name: string}[]} = await response.json()

        return result.data.map((item) => {
          return {value: item.name, ...item}
        })
      },
    }),
   // ...rest of plugins
  ],
})
// post.ts
import {defineField, defineType} from 'sanity'

export default defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({
      name: 'disney',
      type: 'disneyCharacter',
    }),
  ],
})
The field in your Sanity document fetching remote data