Upload, query, and delete assets
Programmatically manage assets in your project dataset.
In cases where the UI doesn't offer enough control or automation, you can use Sanity's APIs to interact with assets.
The techniques in this guide apply to assets stored in a project's dataset. To work with Media Library assets, see Upload assets programmatically in the Media Library documentation.
Dataset assets are always public
Assets stored in a dataset are public on the asset CDN, even when the dataset is private. A private dataset stops unauthenticated users from finding asset URLs through the API, and each URL contains a hash of the file's contents, so it's impractical to guess. Anyone who has the URL can still fetch the file.
Per-asset visibility and signed URLs are available only for Media Library assets. To restrict access to files, use Media Library asset visibility or host the files yourself.
For details on rendering and transforming assets for your front-end applications, see the Presenting images documentation.
Prerequisites:
- For the Node.js examples, Node.js 22.12 or later and
@sanity/clientv8. The client is ESM-only and the examples use top-levelawait, so save them as.mjsfiles or set"type": "module"inpackage.json. - Your project ID and dataset name, which replace
<your-project-id>and<your-dataset>in the examples. - A robot token with write access to the dataset, stored in the
SANITY_API_TOKENenvironment variable. See Authentication and tokens. The Studio example uses the signed-in user's credentials and doesn't need a token.
Upload an asset
Gotcha
Assets are deduplicated by content hash, so uploading the same file under different filenames creates only one asset. See the Assets documentation for details.
In cases where uploading assets in the UI is impractical, like batch uploads or migrations, you can use the client or API directly.
Upload an image and update a draft
The JS Client example below uploads an image and adds its reference to a draft of post-123. Replace this ID with an existing document ID. The edit action updates the existing draft, or creates one from the published document if there is no draft.
Review and publish the draft in Studio when all its changes are ready. Until then, the published document stays unchanged. See Mutate documents with actions for the draft and publishing workflow.
import {createClient} from '@sanity/client'
import {basename} from 'path'
import {createReadStream} from 'fs'
const client = createClient({
projectId: '<your-project-id>',
dataset: '<your-dataset>',
apiVersion: '2026-03-01',
useCdn: false,
token: process.env.SANITY_API_TOKEN
})
const filePath = './images/bicycle.jpg'
const documentId = 'post-123'
const imageAsset = await client.assets.upload('image', createReadStream(filePath), {
filename: basename(filePath)
})
await client.action({
actionType: 'sanity.action.document.edit',
publishedId: documentId,
draftId: `drafts.${documentId}`,
patch: {
set: {
theImageField: {
_type: 'image',
asset: {
_type: 'reference',
_ref: imageAsset._id
}
}
}
}
})
console.log('Image uploaded and added to the draft. Publish the document when ready.')import {useClient} from 'sanity'
export function UploadNotesButton() {
const client = useClient({apiVersion: '2026-03-01'})
async function uploadFile() {
const file = new File(['Hello'], 'notes.txt', {type: 'text/plain'})
try {
const document = await client.assets.upload('file', file)
console.log('The file was uploaded!', document)
} catch (error) {
console.error('Upload failed:', error)
}
}
return (
<button type="button" onClick={uploadFile}>
Upload notes.txt
</button>
)
}curl \
-X POST \
-H "Authorization: Bearer $SANITY_API_TOKEN" \
-H 'Content-Type: image/jpeg' \
--data-binary "@./images/bicycle.jpg" \
'https://YOUR_PROJECT_ID.api.sanity.io/v2026-03-01/assets/images/<your-dataset>'{
"_id": "image-7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a-538x538-jpg",
"_type": "sanity.imageAsset",
"assetId": "7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a",
"path": "images/myproject/mydataset/7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a-538x538.jpg",
"url": "https://cdn.sanity.io/images/myproject/mydataset/7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a-538x538.jpg",
"originalFilename": "bicycle.jpg",
"size": 2097152,
"metadata": {
"dimensions": {
"height": 538,
"width": 538,
"aspectRatio": 1.0
},
"location": {
"lat": 59.9241370,
"lon": 10.7583846,
"alt": 21.0
}
}
}The client.assets.upload() method resolves to the asset document shown in the Asset response shape tab. The HTTP API returns the same document wrapped in a document property. In both, size is in bytes, and metadata.location is present only if you request location metadata (extract: ['location'] in the client, meta=location in the HTTP API) and the image contains location data.
To upload an image or file from an HTTPS URL, use the /from-url endpoints in the Assets API reference. At this time, the client does not support uploading from a URL with the assets.upload method.
Query and browse assets
To browse project assets from the Studio interface, install the Sanity Media plugin. This adds a new tool to the toolbar and enables Studio users to browse and manage assets.
You can also query image and file assets with GROQ. These queries return up to 20 assets each, with their ID, URL, and original filename:
*[_type == "sanity.imageAsset"][0...20]{
_id,
url,
originalFilename
}*[_type == "sanity.fileAsset"][0...20]{
_id,
url,
originalFilename
}[
{
"_id": "image-7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a-538x538-jpg",
"url": "https://cdn.sanity.io/images/myproject/mydataset/7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a-538x538.jpg",
"originalFilename": "bicycle.jpg"
}
]Run GROQ queries through Vision, the client, or the Query API.
You can also use this method to query Media Library assets that have been linked to your dataset.
To view an asset's metadata or URL from a document that references it, follow the asset reference. For example:
*[_type == 'post'] {
mainImage {
asset->
}
}[
{
"mainImage": {
"asset": {
"_id": "image-7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a-538x538-jpg",
"_type": "sanity.imageAsset",
"originalFilename": "bicycle.jpg",
"url": "https://cdn.sanity.io/images/myproject/mydataset/7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a-538x538.jpg"
}
}
}
]Delete assets
To delete an asset, delete its asset document.
import {createClient} from '@sanity/client'
const config = {
projectId: '<your-project-id>',
dataset: '<your-dataset>',
apiVersion: '2026-03-01',
useCdn: false,
token: process.env.SANITY_API_TOKEN
}
const client = createClient(config)
// Note: this is the _id of the asset document.
const result = await client.delete('image-7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a-538x538-jpg')
console.log('deleted image asset', result)Content Lake rejects the delete if other documents still hold a strong reference to the asset, such as an image field that points to it. The error names the referencing documents, for example: Document "image-7f3c9a1e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a-538x538-jpg" cannot be deleted as there are references to it from "post-123". Remove those references, or make them weak, then delete the asset.
While the file is deleted, the CDN might have your asset cached, so it may not disappear immediately.
Download assets
To download an asset, append ?dl=YOUR_FILENAME.jpg to the asset URL. If you leave the filename blank, the original filename is used if present. If the original filename isn't available, the file's ID is used instead.
// GROQ query
*[_type == "post"] {
title,
"imageUrl": mainImage.asset->url
}
// Then you can use the URL in HTML for example like this:
// <a href={`${imageUrl}?dl=`}>Hero Image</a>Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?