Upload assets programmatically
Programmatically upload assets to your Media Library.
Users can upload assets to Media Library directly from the app or from a configured studio. Developers can upload assets with the CLI and the Media Library HTTP API. In this guide, you'll learn to upload an asset to your library programmatically.
Looking for details on uploading to Media Library from the app? See the Media Library user guide.
Looking to upload to a single project?
This guide discusses uploading assets to Media Library. For information on uploading assets to your project, see the Content Lake guide on uploading assets.
Upload an asset
Option 1: use the CLI
The Sanity CLI includes the media import command that standardizes the process for uploading batches of assets and simultaneously assigning aspect information. More detail is available in our guide for importing assets.
To use the CLI with the media commands, you must run the command in a directory containing a sanity.cli.ts configuration file. Learn more about configuring the CLI to recognize your library.
Option 2: use the HTTP API
Prerequisites:
- Node.js v21.0 or later to run the built-in
fetchAPI, or a compatible request library. - A personal authentication token with read/write access to your organization's Media Library.
- The
mediaLibraryId.
Make a POST request to the /media-libraries/MEDIA_LIBRARY_ID/upload endpoint. In the following code, replace MEDIA_LIBRARY_ID with your library's ID, and set the SANITY_API_TOKEN environment variable to your personal authentication token.
import fs from 'node:fs'
// Define your library ID.
const mediaLibraryId = 'MEDIA_LIBRARY_ID'
// Read your personal auth token from the environment.
const token = process.env.SANITY_API_TOKEN
// Read the contents of a file.
const asset = fs.readFileSync('assets/spring-launch-promo.jpg')
// POST a request to the HTTP API with the file as the body
await fetch(`https://api.sanity.io/v2025-02-19/media-libraries/${mediaLibraryId}/upload`, {
method: 'POST',
body: asset,
headers: {
'Authorization': `Bearer ${token}`,
}
})To set the asset's title and original filename, pass a filename query parameter. To generate a title from the image content, pass autoGenerateTitle=true.
See the Media Library HTTP API reference for all available upload options and parameters.
To upload a new version of an existing asset instead of a new asset, see Update an asset to a new version with the API.
Upload directly into a folder
Pass a parent query parameter to upload an asset directly into a folder (a sanity.directory document) in a single request. In the following code, replace DIRECTORY_ID with the document ID of the destination folder.
import fs from 'node:fs'
const mediaLibraryId = 'MEDIA_LIBRARY_ID'
const token = process.env.SANITY_API_TOKEN
const directoryId = 'DIRECTORY_ID'
const asset = fs.readFileSync('assets/spring-launch-promo.jpg')
await fetch(
`https://api.sanity.io/v2025-02-19/media-libraries/${mediaLibraryId}/upload?parent=${directoryId}`,
{
method: 'POST',
body: asset,
headers: {
Authorization: `Bearer ${token}`,
},
},
)parent must reference a sanity.directory. sanity.tree is not a valid value. Assets in Media Library cannot live directly under the tree, only inside a folder.
Upload with aspect values
To set aspects on the asset in the same call, send the request as multipart/form-data. Add an aspects part containing a JSON object keyed by aspect name, then the file part. The file part must come last: the upload pipeline stops reading after it, so it ignores any part after file. The aspects object has a 1 MiB limit, and each aspect you set must already exist in your library.
import fs from 'node:fs'
const mediaLibraryId = 'MEDIA_LIBRARY_ID'
const token = process.env.SANITY_API_TOKEN
const form = new FormData()
// Add aspects first. The file must be the last part, because parts after it are not read.
form.append(
'aspects',
JSON.stringify({
copyright: {copyrightHolder: 'Example Inc.', copyrightDate: '2026-09-07'},
}),
)
form.append(
'file',
new Blob([fs.readFileSync('assets/spring-launch-promo.jpg')], {type: 'image/jpeg'}),
'spring-launch-promo.jpg',
)
const response = await fetch(`https://api.sanity.io/v2025-02-19/media-libraries/${mediaLibraryId}/upload`, {
method: 'POST',
body: form,
headers: {
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) {
// Upload errors have a JSON body: {"error": {"description": "..."}}
throw new Error(`Upload failed (${response.status}): ${await response.text()}`)
}Use this approach when you import assets in bulk. Uploading first and then patching aspects costs two requests per asset. Setting aspects during upload costs one.
Troubleshoot upload errors
When the upload endpoint rejects a request, it responds with 400 Bad Request and a JSON body in the form {"error": {"description": "..."}}. These are the most common descriptions for uploads with aspects:
missing 'file' part in multipart request: The multipart body has no part namedfile. Append the asset with the form field namefile.aspects part exceeds maximum size: Theaspectspart is larger than 1 MiB. Send less aspect data with the upload, and set the rest with a mutation after the asset exists.aspects field is not valid JSON: …: Theaspectspart doesn't parse as a JSON object. Build it withJSON.stringify()on an object keyed by aspect name.aspects cannot be combined with assetId; aspects only apply when creating a new asset: The request has anassetIdquery parameter, which adds a version to an existing asset. Remove theaspectspart, and set aspect values on the existing asset with a mutation.
Parts after the file part are skipped without an error. If an uploaded asset is missing its aspects, check that the aspects part comes before the file part.
Download the original asset
The Media Library CDN serves an optimized rendition of each image by default. To download the original file, add the dlRaw parameter to the asset's CDN URL and send a bearer token with the request. Without a token, the request returns 401 Unauthorized. In the following code, set assetUrl to the CDN URL of your image. In the example URL, MEDIA_LIBRARY_ID is your library's ID and ASSET_ID-4000x6000.jpg is the image's file name.
const token = process.env.SANITY_API_TOKEN
const assetUrl =
'https://cdn.sanity.io/media-libraries/MEDIA_LIBRARY_ID/images/ASSET_ID-4000x6000.jpg'
const response = await fetch(`${assetUrl}?dlRaw=`, {
headers: {
Authorization: `Bearer ${token}`,
},
})dlRaw works like dl, which downloads the optimized rendition and needs no token for public assets. For details on both parameters, see Image transformations.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?