File uploads - Allow users of my app to upload assets (PDF or images) through a file picker.
I totally understand your frustration! The HTTP API docs showing a file path example is confusing when you're working with browser file inputs. Good news: you don't need to use file paths at all when uploading from the browser.
The key is using the Sanity JavaScript client with its client.assets.upload() method, which accepts a browser File object directly. Here's how to do it:
Basic Browser Upload Example
import {createClient} from '@sanity/client'
const client = createClient({
projectId: 'your-project-id',
dataset: 'your-dataset',
token: 'your-write-token', // Important: needs write permissions
apiVersion: '2024-01-01',
useCdn: false
})
// Handle file input change
async function handleFileUpload(event) {
const file = event.target.files[0]
if (!file) return
try {
// Upload the file directly - works with File object from input!
const asset = await client.assets.upload('file', file, {
filename: file.name
})
// Response contains everything you need:
console.log('File URL:', asset.url)
console.log('File name:', asset.originalFilename)
console.log('Asset ID:', asset._id)
// Save to your database
await saveToYourDatabase({
url: asset.url,
filename: asset.originalFilename,
assetId: asset._id
})
} catch (error) {
console.error('Upload failed:', error)
}
}HTML Setup
<input type="file" accept=".pdf,image/*" onChange={handleFileUpload} />Important Setup Notes
1. Authentication Token: You need a token with write permissions. Create one in your Sanity project settings under API → Tokens.
2. CORS Configuration: If uploading from the browser, add your domain to the CORS origins in your project settings (Sanity Manage → API → CORS Origins).
3. File Size Limits: Sanity has a default upload limit of around 200MB for most plans, but check your specific plan limits.
For Images vs Other Files
The method is slightly different depending on file type:
// For images
const imageAsset = await client.assets.upload('image', file, {
filename: file.name
})
// For PDFs and other files
const fileAsset = await client.assets.upload('file', file, {
filename: file.name
})Complete React Example
import {useState} from 'react'
import {createClient} from '@sanity/client'
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
token: process.env.SANITY_WRITE_TOKEN,
apiVersion: '2024-01-01',
useCdn: false
})
function FileUploader() {
const [uploading, setUploading] = useState(false)
const [uploadedFile, setUploadedFile] = useState(null)
const handleUpload = async (e) => {
const file = e.target.files[0]
if (!file) return
setUploading(true)
try {
const asset = await client.assets.upload('file', file, {
filename: file.name
})
setUploadedFile({
url: asset.url,
filename: asset.originalFilename,
id: asset._id
})
// Save to your database here
} catch (error) {
console.error('Upload failed:', error)
} finally {
setUploading(false)
}
}
return (
<div>
<input
type="file"
onChange={handleUpload}
disabled={uploading}
/>
{uploading && <p>Uploading...</p>}
{uploadedFile && (
<div>
<p>Uploaded: {uploadedFile.filename}</p>
<a href={uploadedFile.url} target="_blank">View File</a>
</div>
)}
</div>
)
}Using from Within Sanity Studio
If you're building a custom component inside Sanity Studio, you can use the useClient hook instead:
import {useClient} from 'sanity'
// Inside a React component
const client = useClient({apiVersion: '2025-02-19'})
const file = event.target.files[0]
client.assets
.upload('file', file)
.then((document) => {
console.log('The file was uploaded!', document)
})
.catch((error) => {
console.error('Upload failed:', error.message)
})The confusion in the HTTP API docs comes from showing a Node.js example with fs.createReadStream(). In the browser, you simply pass the File object from the file input directly - no file paths needed!
For downloading files later, you can use the URL with the ?dl parameter: ${fileUrl}?dl to force download instead of opening in the browser.
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.