Working with assets in @sanity/client

Learn how to upload, query, and delete assets in both Content Lake and Media Library using the Sanity JavaScript client.

The @sanity/client library provides methods for uploading, querying, and deleting assets in your Sanity project. This guide covers working with assets in Content Lake and introduces Media Library for video workflows.

Before you begin

Upload assets to Content Lake

The client.assets.upload() method handles uploads to Content Lake. It accepts a type ('image' or 'file'), a body (file stream, buffer, Blob, or File object), and an optional configuration object. The method returns a promise that resolves to the created asset document.

Upload from Node.js

In Node.js environments (build scripts, migrations, server-side processing), you can upload from file streams or buffers. Replace and with your project ID and dataset name, and set the SANITY_API_TOKEN environment variable to a token with write access.

import {createClient} from '@sanity/client'
import {createReadStream} from 'fs'
import {basename} from 'path'

const client = createClient({
  projectId: '',
  dataset: '',
  apiVersion: '2026-03-01',
  token: process.env.SANITY_API_TOKEN,
  useCdn: false
})

const filePath = './images/hero.jpg'

try {
  const imageAsset = await client.assets.upload(
    'image',
    createReadStream(filePath),
    {filename: basename(filePath)}
  )

  console.log('Uploaded image:', imageAsset._id)
  console.log('URL:', imageAsset.url)
} catch (error) {
  console.error('Upload failed:', error.message)
}

After uploading, you typically reference the asset from a document. Use patch() to set the asset reference on an existing document. For Media Library assets, see Link assets to documents.

// Upload and attach an image to a document in one step
const imageAsset = await client.assets.upload(
  'image',
  createReadStream('./images/hero.jpg'),
  {filename: 'hero.jpg'}
)

await client
  .patch('post-123')
  .set({
    mainImage: {
      _type: 'image',
      asset: {
        _type: 'reference',
        _ref: imageAsset._id
      }
    }
  })
  .commit()

console.log('Image attached to document')

Upload from the browser

In browser environments, pass a File or Blob object to the upload method.

Don't use a write token in browser code

// Upload a file from an <input type="file"> element
const input = document.querySelector('input[type="file"]') as HTMLInputElement

input.addEventListener('change', async () => {
  const file = input.files?.[0]
  if (!file) return

  try {
    const asset = await client.assets.upload('image', file, {
      filename: file.name
    })
    console.log('Uploaded file:', asset._id)
  } catch (error) {
    console.error('Upload failed:', error.message)
  }
})

Track upload progress

The promise-based client.assets.upload() resolves once the upload finishes and reports nothing in between. To show progress, use the observable variant, client.observable.assets.upload(). It emits progress events while the file transfers and ends with a single response event that carries the asset document:

// Track progress while uploading from an <input type="file"> element
const input = document.querySelector('input[type="file"]') as HTMLInputElement
const progressBar = document.querySelector('progress') as HTMLProgressElement
// percent runs from 0 to 100, but <progress> defaults to max = 1
progressBar.max = 100

input.addEventListener('change', () => {
  const file = input.files?.[0]
  if (!file) return

  client.observable.assets
    .upload('image', file, {filename: file.name})
    .subscribe({
      next: (event) => {
        if (event.type === 'progress') {
          // percent is 0 when the browser can't compute the total
          progressBar.value = event.percent
          return
        }
        // The response event carries the asset document
        console.log('Uploaded asset:', event.body.document._id)
      },
      error: (error) => console.error('Upload failed:', error),
      complete: () => console.log('Upload complete')
    })
})

Each progress event has a percent value from 0 to 100 and the raw loaded and total byte counts. When the browser can't determine the total, lengthComputable is false, percent stays at 0, and total is undefined.

Progress events are browser-only

In @sanity/client v8 and later, the event types are exported as UploadEvent, UploadProgressEvent, and UploadResponseEvent. The ResponseEvent and ProgressEvent types from earlier versions are removed. See Migrating from v7 for the before-and-after.

Specify image metadata to extract

When uploading images, use the extract option to control which metadata is processed during upload. Passing extract replaces the default set (blurhash, thumbhash, lqip, and palette), so list every value you need. Available values include:

  • palette: Extracts dominant colors from the image.
  • location: Extracts GPS coordinates from EXIF data, if available.
  • exif: Extracts EXIF metadata like camera settings and timestamps.
  • blurhash: Generates a compact placeholder representation.
  • lqip: Generates a 20-pixel-wide, base64-encoded preview image.
  • thumbhash: Generates a compact placeholder that also encodes aspect ratio and transparency.
  • image: Extracts basic image information like camera make and model, resolution, and orientation.
const imageAsset = await client.assets.upload(
  'image',
  createReadStream('./images/photo.jpg'),
  {
    filename: 'photo.jpg',
    extract: ['palette', 'lqip', 'exif', 'location', 'blurhash']
  }
)

console.log('Palette:', imageAsset.metadata.palette)
console.log('Dimensions:', imageAsset.metadata.dimensions)

Query assets

Assets in Content Lake are documents with the types sanity.imageAsset and sanity.fileAsset. You can query them with GROQ like any other document. For rendering images on your front end, see Presenting images. You can also manage assets through the Assets HTTP API.

// Fetch up to 50 image assets
const images = await client.fetch('*[_type == "sanity.imageAsset"][0...50]')

// Fetch up to 50 file assets
const files = await client.fetch('*[_type == "sanity.fileAsset"][0...50]')

// Follow a reference to get asset metadata from a document
const posts = await client.fetch(`
  *[_type == 'post'][0...10] {
    title,
    mainImage {
      asset-> {
        _id,
        url,
        metadata {
          dimensions,
          palette
        }
      }
    }
  }
`)

Delete assets from Content Lake

Delete an asset by passing its document ID to client.delete(). This permanently removes the asset and its associated data.

try {
  await client.delete('image-abc123-300x200-jpg')
  console.log('Asset deleted')
} catch (error) {
  if (error.statusCode === 409) {
    // Document "<asset-id>" cannot be deleted as there are references to it from "<document-id>"
    console.error('Asset is still referenced by other documents')
  } else {
    console.error('Delete failed:', error.message)
  }
}

Remove references before deleting an asset

Work with Media Library

Media Library is a specialized storage and delivery system for managing assets across your organization. It provides features like adaptive streaming, automatic transcoding, and thumbnail generation for video content. Unlike Content Lake assets, Media Library assets are stored separately and managed through a dedicated API.

This is a paid feature

This feature is available as an addon for certain Enterprise plans. Talk to sales to learn more.

Configure the client for Media Library

To query or interact with Media Library from @sanity/client, configure the client with a resource property pointing to your Media Library instance.

import {createClient} from '@sanity/client'

const client = createClient({
  apiVersion: '2026-03-01',
  useCdn: false,
  token: process.env.SANITY_API_TOKEN,
  resource: {
    type: 'media-library',
    id: 'YOUR_MEDIA_LIBRARY_ID'
  }
})

Or, if you have a configured client and want to use the same configuration, use withConfig.

import {client} from './lib/client' // import your configured client

const mlClient = client.withConfig({
  token: process.env.SANITY_API_TOKEN,
  resource: {
    type: 'media-library',
    id: 'YOUR_MEDIA_LIBRARY_ID'
  }
})

Replace YOUR_MEDIA_LIBRARY_ID with your Media Library ID. You can find it in the Sanity management console under your organization settings. See Configure your library for setup details.

Query Media Library assets

With a Media Library-configured client, you can query assets using GROQ.

// Query up to 50 assets in your Media Library
const assets = await client.fetch('*[_type == "sanity.asset"][0...50]')

// Query up to 50 image assets
const images = await client.fetch(
  '*[assetType == "sanity.imageAsset"][0...50]'
)

// Query up to 50 recent assets
const recentAssets = await client.fetch(
  '*[_type == "sanity.asset" && _createdAt > $date][0...50]',
  {date: '2026-01-01'}
)

Upload to Media Library

Upload assets to Media Library using the client.assets.upload() method on a Media Library-configured client.

Note on upload category

import fs from 'node:fs'

// Upload an image
const imageAsset = await client.assets.upload(
  'image',
  fs.createReadStream('photo.jpg'),
  {
    filename: 'photo.jpg',
    title: 'Product Photo'
  }
)

console.log('Uploaded:', imageAsset._id)

// Upload a video. Use 'file' here because the JavaScript client only accepts
// 'image' or 'file'; Media Library still detects the MP4 as a video.
const videoAsset = await client.assets.upload(
  'file',
  fs.createReadStream('promo.mp4'),
  {filename: 'promo.mp4'}
)

console.log('Uploaded video:', videoAsset._id)

Delete Media Library assets

Media Library uses the same mutation API as Content Lake for deletions. To ensure both the published and draft versions are removed, use a transaction.

const assetId = '36fOGtOJOadpl4F9xpksb9uKjYp'

// Delete both the asset and its draft
try {
  await client
    .transaction()
    .delete(assetId)
    .delete(`drafts.${assetId}`)
    .commit()

  console.log('Asset deleted from Media Library')
} catch (error) {
  console.error('Delete failed:', error.message)
}

Get video playback information

For video assets, use client.mediaLibrary.video.getPlaybackInfo() to retrieve playback URLs, thumbnails, and metadata like duration and aspect ratio. See Working with video for a complete guide to video delivery.

// Fetch a document with a video reference
const doc = await client.fetch(
  `*[_type == 'videoPage'][0]{ title, video }`
)

// Get playback info from the video asset reference
const playbackInfo = await client.mediaLibrary.video.getPlaybackInfo(
  doc.video.asset
)

console.log('Stream URL:', playbackInfo.stream.url)
console.log('Thumbnail:', playbackInfo.thumbnail.url)
console.log('Duration:', playbackInfo.duration)
console.log('Aspect ratio:', playbackInfo.aspectRatio)

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme