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
This guide assumes you have a configured Sanity client. Server-side examples use a token with write access from SANITY_API_TOKEN. See Getting started with @sanity/client for setup instructions. Media Library sections require an active Media Library on your organization. @sanity/client v8 requires Node.js 22.12 or later.
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 <your-project-id> and <your-dataset> 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: '<your-project-id>',
dataset: '<your-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
Anyone who loads the page can read a token bundled into browser code and use it to change your dataset. In the browser, create the client without a token and set withCredentials: true so uploads use the logged-in user's Sanity session. The page's origin must be an allowed CORS origin with credentials enabled. Otherwise, send the file to a server route that uploads it with a token kept on the server.
// 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
Progress events come from XMLHttpRequest, so the client only emits them in a browser. In Node.js and edge runtimes, the observable emits the response event alone. Nothing throws, so a Node.js progress bar built on these events stays at 0 until the upload completes. This applies to @sanity/client v8 and later. Earlier versions emitted progress events on Node.js as well.
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
}
}
}
}
`)[
{
"title": "Hiking the Lofoten Islands",
"mainImage": {
"asset": {
"_id": "image-3f8a2c9d1e7b4a6f0c5d8e2b9a1f4c7d6e0b3a58-3024x4032-jpg",
"url": "https://cdn.sanity.io/images/YOUR_PROJECT_ID/<your-dataset>/3f8a2c9d1e7b4a6f0c5d8e2b9a1f4c7d6e0b3a58-3024x4032.jpg",
"metadata": {
"dimensions": {
"_type": "sanity.imageDimensions",
"aspectRatio": 0.75,
"height": 4032,
"width": 3024
},
"palette": {
"_type": "sanity.imagePalette",
"dominant": {
"_type": "sanity.imagePaletteSwatch",
"background": "#d5c3ba",
"foreground": "#000",
"population": 7.17,
"title": "#fff"
}
// ... darkMuted, darkVibrant, lightMuted, lightVibrant, muted, vibrant
}
}
}
}
}
]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
If any document holds a strong reference to the asset, the delete fails with a 409 status code and the message Document "<asset-id>" cannot be deleted as there are references to it from "<document-id>". Remove or replace those references first, then delete the asset. Weak references (_weak: true) don't block the delete, but they're left pointing at an asset that no longer exists.
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
The first argument to client.assets.upload() is the JavaScript client’s upload category and must be either 'image' or 'file'. For Media Library uploads, this argument does not set the Media Library HTTP type override. Media Library uploads are sent to /media-libraries/{id}/upload, and the asset type is detected from the uploaded content when no HTTP type override is provided. Use 'file' when uploading videos through the JavaScript client.
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?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?