DAM & Media8 min read

How to Integrate Bunny.net with Your Headless CMS

Connect Bunny.net to your headless CMS so published media, metadata, and cache updates move from structured content to Bunny’s edge automatically.

Published April 29, 2026
01Overview

What is Bunny.net?

Bunny.net is a global edge platform for CDN delivery, edge storage, image resizing, video streaming, DNS, and security. Teams use it to serve images, downloads, and video close to users through Bunny Storage Zones, Pull Zones, Stream, and its API. It competes in the CDN and media delivery market with self-serve setup, usage-based pricing, and a developer-facing API.


02The case for integration

Why integrate Bunny.net with a headless CMS?

Media gets messy when editorial content and delivery infrastructure live in different places. An editor publishes a product page with a new hero image, but the image still needs to be uploaded to Bunny Storage, attached to the right Pull Zone URL, purged from cache, and referenced correctly on the frontend. If that work is manual, you get stale images, broken URLs, and last-minute Slack messages asking which file is actually live.

Connecting Bunny.net to a headless CMS category tool solves that by treating media delivery as part of the publishing flow. With Sanity, content is structured as typed JSON in the Content Lake, so a webhook or Function can read exactly which image, video, slug, locale, or campaign changed. GROQ selects the fields Bunny.net needs, and the sync logic can upload files to Bunny Storage, purge a CDN URL, or update derived media fields without polling every few minutes.

The trade-off is that you need to define the rules up front. For example, should every image publish to Bunny Storage, or only images tagged for a marketing site? Should deletes remove files from Bunny, or keep them for rollback? Those decisions take a little planning, but the payoff is clear: editors keep working in Sanity Studio, developers keep media delivery on Bunny.net, and the frontend receives fast, predictable URLs.


03Architecture

Architecture overview

A typical Sanity and Bunny.net integration starts when an editor publishes or updates a document in Sanity Studio. The content lands in the Content Lake as structured JSON, with references to Sanity assets, captions, alt text, slugs, locales, and any delivery settings you model in your schema. A Sanity webhook fires on the publish event. You can filter the webhook with GROQ so it only triggers for documents that include media fields or a specific flag like syncToBunny == true. The webhook sends the document ID to a server endpoint, or you can run the same logic in Functions so the sync happens server-side without standing up separate infrastructure. The sync handler uses @sanity/client and GROQ to fetch the current document shape, including joined asset fields such as image.asset->url, file.asset->mimeType, slug.current, and any Bunny path you’ve modeled. It downloads the source asset, calls Bunny.net’s Storage API with a PUT request to https://storage.bunnycdn.com/{storageZoneName}/{path}, and authenticates with the Storage Zone access key in the AccessKey header. If the file is already cached through a Pull Zone, the handler can call Bunny.net’s purge API at https://api.bunny.net/purge?url={cdnUrl} using the account API key. The end user never touches Sanity directly. Your website, app, or agent queries Sanity for the content structure and uses the Bunny CDN URL for the media asset. Bunny.net handles edge delivery, while Sanity remains the source for titles, alt text, captions, routing, localization, references, and publish state.


04Use cases

Common use cases

🖼️

Publish-ready image delivery

Sync hero images, campaign graphics, and product photos from Sanity publish events into Bunny Storage, then serve them through a Pull Zone URL.

🎬

Video landing pages

Model video pages in Sanity, store titles, transcripts, CTAs, and publish state there, and deliver the media through Bunny Stream or Bunny CDN.

🌍

Localized media paths

Use Sanity fields like locale, market, and slug to create Bunny paths such as /en-us/summer-sale/hero.jpg and /fr-ca/summer-sale/hero.jpg.

🧹

Cache purge on publish

Trigger Bunny.net cache purges when an editor replaces a file, updates a campaign image, or republishes a media-heavy page.


05Implementation

Step-by-step integration

  1. 1

    Set up Bunny.net delivery

    Create a Bunny.net account, then create a Storage Zone for uploaded media. Connect it to a Pull Zone, note the Storage Zone name, storage hostname, CDN hostname, Storage Zone access key, and account API key. If you’re serving large videos, set up Bunny Stream separately and decide whether Sanity will hold Stream video IDs, embed URLs, or both.

  2. 2

    Install the Sanity client

    In your webhook handler, Function, or middleware project, install @sanity/client. Bunny.net’s Storage API works over HTTP, so a standard fetch call is enough for basic uploads, deletes, and cache purges.

  3. 3

    Model Bunny delivery fields in Sanity Studio

    Add fields such as heroImage, altText, syncToBunny, bunnyPath, bunnyCdnUrl, locale, and slug. Keep editor-facing fields clear. For example, editors should choose whether an asset syncs to Bunny, but your code should generate the final path to avoid typos.

  4. 4

    Create the sync trigger

    Create a Sanity webhook that fires on publish for the document types that need Bunny delivery. Use a GROQ filter such as _type == 'article' && syncToBunny == true. Point it at your webhook listener, or use Functions if you want the server-side logic to run from Sanity events without a separate service.

  5. 5

    Upload to Bunny.net and purge cache

    Fetch the latest document from the Content Lake with GROQ, download the Sanity asset URL, and PUT the bytes to Bunny Storage at /{storageZoneName}/{path}. After upload, call Bunny.net’s purge API for the CDN URL so visitors don’t receive the previous file.

  6. 6

    Test the frontend path

    Publish a draft, confirm the file exists in Bunny Storage, confirm the Pull Zone URL returns a 200, and render the Bunny CDN URL in your frontend. Also test a replacement image, an unpublished document, and a failed upload so your team knows what happens when the sync doesn’t finish.


06Code

Code example

ts
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 {id} = await req.json();

  const doc = await sanity.fetch(
    `*[_id == $id][0]{
      title,
      "slug": slug.current,
      "imageUrl": heroImage.asset->url
    }`,
    {id}
  );

  if (!doc?.imageUrl || !doc?.slug) {
    return Response.json({skipped: true});
  }

  const image = await fetch(doc.imageUrl);
  const bytes = await image.arrayBuffer();
  const path = `articles/${doc.slug}/hero.jpg`;
  const storageHost = process.env.BUNNY_STORAGE_HOST || 'storage.bunnycdn.com';
  const zone = process.env.BUNNY_STORAGE_ZONE!;

  const upload = await fetch(`https://${storageHost}/${zone}/${path}`, {
    method: 'PUT',
    headers: {
      AccessKey: process.env.BUNNY_STORAGE_KEY!,
      'Content-Type': 'application/octet-stream',
    },
    body: bytes,
  });

  if (!upload.ok) {
    throw new Error(`Bunny upload failed: ${upload.status}`);
  }

  const cdnUrl = `https://${process.env.BUNNY_CDN_HOST}/${path}`;
  await fetch(`https://api.bunny.net/purge?url=${encodeURIComponent(cdnUrl)}`, {
    method: 'POST',
    headers: {AccessKey: process.env.BUNNY_API_KEY!},
  });

  return Response.json({synced: true, cdnUrl});
}

07Why Sanity

How Sanity + Bunny.net works

Build your Bunny.net integration on Sanity

Sanity gives you the structured content foundation, real-time event system, and flexible APIs to connect Bunny.net to your publishing workflow.

Start building free →

08Comparison

CMS approaches to Bunny.net

CapabilityTraditional CMSSanity
Media-to-content mappingMedia is often attached inside page fields or plugins, which can make automated Bunny paths hard to generate consistently.Schemas define the exact fields Bunny needs, including asset references, slugs, locales, alt text, and sync flags.
Publish-time Bunny syncA plugin or manual upload often handles the transfer, and failures may not match the editorial publish flow.Webhooks or Functions can run sync logic on publish, including upload to Bunny Storage and cache purge calls.
Field-level query controlIntegrations may read rendered HTML, full pages, or plugin-specific payloads.GROQ fetches the exact fields and references needed for Bunny.net in one query.
Cache invalidationCache purge logic is often tied to page publish events, not specific media replacements.Schema fields and GROQ projections can generate the exact Bunny CDN URL to purge when an asset changes.
Multi-channel media deliveryWeb pages are usually the primary target, and reuse across apps or agents can take extra work.The same structured content can feed web, mobile, Bunny.net, and production AI agents through Agent Context.

09Next 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 Bunny.net and 200+ other tools.