DAM & Media8 min read

How to Integrate Cloudinary with Your Headless CMS

Connect Cloudinary to structured content so product pages, campaigns, and apps can publish resized, tagged, CDN-delivered media without copy-pasting URLs.

Published April 29, 2026
01 — Overview

What is Cloudinary?

Cloudinary is a cloud-based media platform for uploading, transforming, organizing, and delivering images and videos through a CDN. Teams use it as a DAM, media API, and delivery layer for ecommerce, publishing, SaaS, and marketing sites. Its core strength is programmatic media handling, including transformations, responsive delivery, metadata, folders, tags, and signed uploads.


02 — The case for integration

Why integrate Cloudinary with a headless CMS?

Media gets messy fast when your content and assets live in separate systems. A merchandiser updates a product title in one place, a designer uploads a new hero image in another, and the frontend still points at last month’s Cloudinary URL. At 50 assets, that’s annoying. At 50,000 assets across product pages, landing pages, emails, and apps, it turns into broken alt text, stale campaign images, and duplicate uploads.

Connecting Cloudinary to a headless CMS lets content changes trigger media work automatically. When a product, article, or campaign is published, the integration can send the right image URL, alt text, caption, tags, folder path, and product category to Cloudinary. Cloudinary can then create the derived assets your frontend needs, such as WebP, AVIF, cropped thumbnails, social cards, and responsive breakpoints.

The difference is structure. With Sanity’s AI Content Operating System, content in the Content Lake is typed JSON, not page HTML or opaque blobs. GROQ can select exactly the fields Cloudinary needs, webhooks can fire on publish events, and Functions can run the server-side sync code without a separate queue worker. The trade-off is that you’ll need to design the schema and sync rules up front. That’s extra setup, but it prevents the long-term pain of manual uploads, copied URLs, and metadata that drifts out of date.


03 — Architecture

Architecture overview

A typical Sanity and Cloudinary integration starts with structured content in the Content Lake. For example, a product document might include a title, slug, category reference, Sanity image asset, alt text, locale, and a Cloudinary field for the returned public ID and secure URL. On publish, a Sanity webhook can use a GROQ filter such as `_type == "product" && defined(mainImage.asset)` so only relevant mutations trigger the integration. The webhook sends a small payload, often just `_id`, to a Sanity Function or a server endpoint. That server-side code uses `@sanity/client` to fetch the full document with GROQ, including joins like `category->title` and `mainImage.asset->url`. The function then calls Cloudinary’s Node SDK, usually `cloudinary.uploader.upload()` for a Sanity-hosted image URL or `cloudinary.uploader.explicit()` if the asset already exists in Cloudinary and only needs updated metadata. The request can set `folder`, `public_id`, `tags`, `context`, `overwrite`, and transformation settings. Cloudinary returns fields such as `public_id`, `secure_url`, `version`, `width`, `height`, and `format`. You can write those values back to Sanity, render them directly on the frontend, or store only the Cloudinary public ID and build delivery URLs at request time. From there, the end user gets media from Cloudinary’s CDN, while the page, app, or AI agent gets structured content from Sanity. When an editor changes the product title, alt text, campaign tag, or source image, the webhook runs again and Cloudinary receives the update without polling.


04 — Use cases

Common use cases

🛍️

Product media sync

Send product images, alt text, SKU tags, and category metadata from Sanity to Cloudinary when a product is published.

🖼️

Campaign image variants

Create Cloudinary transformations for hero banners, email headers, social cards, and mobile crops from one approved campaign asset.

🌍

Localized media delivery

Use Sanity locale fields to tag Cloudinary assets by market, language, region, or legal usage rights.

🎥

Video metadata publishing

Keep Cloudinary video posters, captions, titles, and taxonomy aligned with the structured content your editors publish.


05 — Implementation

Step-by-step integration

  1. 1

    Set up Cloudinary

    Create a Cloudinary account, copy your cloud name, API key, and API secret from the dashboard, then install the Node SDK with `npm install cloudinary`.

  2. 2

    Model media fields in Sanity Studio

    Add fields for source image, alt text, caption, rights, locale, tags, and a `cloudinary` object that can store `publicId`, `secureUrl`, `version`, `width`, and `height`.

  3. 3

    Create a publish trigger

    Add a Sanity webhook with a GROQ filter like `_type == "product" && defined(mainImage.asset)` or run the same logic in a Sanity Function triggered by content mutations.

  4. 4

    Fetch only the fields Cloudinary needs

    Use GROQ to fetch the document ID, slug, title, category, image URL, alt text, and any tags or locale fields that should become Cloudinary metadata.

  5. 5

    Call Cloudinary’s Upload API

    Use `cloudinary.uploader.upload(imageUrl, { folder, public_id, tags, context, overwrite: true })` to register or replace the media asset and attach structured metadata.

  6. 6

    Test delivery in your frontend

    Render Cloudinary `secure_url` values or build transformation URLs with Cloudinary’s SDK, then test publish, update, delete, and rollback cases before turning on the webhook for all content.


06 — Code

Code example

typescriptapp/api/sanity-to-cloudinary/route.ts
import { createClient } from '@sanity/client'
import { v2 as cloudinary } from 'cloudinary'

const sanity = createClient({
  projectId: process.env.SANITY_PROJECT_ID!,
  dataset: process.env.SANITY_DATASET!,
  apiVersion: '2025-01-01',
  token: process.env.SANITY_WRITE_TOKEN!,
  useCdn: false
})

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
  api_key: process.env.CLOUDINARY_API_KEY!,
  api_secret: process.env.CLOUDINARY_API_SECRET!
})

export async function POST(req: Request) {
  const { _id } = await req.json()

  const product = await sanity.fetch(`*[_id == $_id][0]{
    _id,
    title,
    "slug": slug.current,
    "category": category->title,
    "imageUrl": mainImage.asset->url,
    "alt": mainImage.alt
  }`, { _id })

  if (!product?.imageUrl) {
    return Response.json({ skipped: true })
  }

  const uploaded = await cloudinary.uploader.upload(product.imageUrl, {
    folder: 'products',
    public_id: product.slug,
    overwrite: true,
    tags: ['sanity', product.category].filter(Boolean),
    context: {
      alt: product.alt || '',
      title: product.title,
      sanity_id: product._id
    }
  })

  await sanity.patch(product._id).set({
    cloudinary: {
      publicId: uploaded.public_id,
      secureUrl: uploaded.secure_url,
      version: uploaded.version,
      width: uploaded.width,
      height: uploaded.height
    }
  }).commit()

  return Response.json({ publicId: uploaded.public_id })
}

07 — Why Sanity

How Sanity + Cloudinary works

Build your Cloudinary integration on Sanity

Sanity gives you the structured content foundation, real-time event system, and flexible APIs to connect Cloudinary with your media workflow.

Start building free →

08 — Comparison

CMS approaches to Cloudinary

CapabilityTraditional CMSSanity
Media metadata shapeMedia fields are often tied to page templates, so syncing alt text, rights, and taxonomy to Cloudinary usually requires custom parsing.Typed JSON in the Content Lake keeps media, alt text, references, locale, and Cloudinary IDs in one queryable structure.
Publish-triggered syncPlugins or cron jobs often push assets after a save, which can miss edge cases like scheduled publishing or rollbacks.GROQ-filtered webhooks and Functions can run Cloudinary sync logic on specific content mutations without polling.
Field-level query controlIntegrations may receive full page payloads, then discard fields Cloudinary doesn’t need.GROQ can project only the Cloudinary payload, including joined references, computed fields, and fallback values.
Editorial workflow for assetsEditors often paste Cloudinary URLs into rich text or page fields, which makes validation difficult.Sanity Studio can add validation for alt text, rights, required crops, Cloudinary public IDs, and market-specific metadata.
Multi-channel deliveryThe website is usually the primary target, so apps, feeds, and agents may need separate export logic.One structured source can feed websites, apps, Cloudinary, marketing automation, and Agent Context for AI agents.

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 Cloudinary and 200+ other tools.