Create a new upload
⚠️ We highly advocate for utilizing our JavaScript client when uploading new assets, as it comes equipped with high-level helper methods that handle all the nitty-gritty for you.
Uploading a new asset to a DatoCMS project manually is a process that necessitates several HTTP calls. This approach significantly accelerates the upload process for DatoCMS, as it eliminates the need for us to act as an intermediary between you, the user aiming to upload an asset, and the storage bucket.
File uploads via HTTP is a multi-step process.
In this first step, you send a POST request to the /upload-requests endpoint to obtain a presigned bucket URL for later use. No actual file upload occurs yet; you're just authenticating and retrieving necessary metadata.
POST https://site-api.datocms.com/upload-requests HTTP/1.1Authorization: Bearer YOUR-API-TOKENAccept: application/jsonX-Api-Version: 3Content-Type: application/vnd.api+json
{ "data": { "type": "upload_request", "attributes": { "filename": "image.png" } }}curl -g 'https://site-api.datocms.com/upload-requests' \ -X POST \ -H "Authorization: Bearer YOUR-API-TOKEN" \ -H "Accept: application/json" \ -H "X-Api-Version: 3" \ -H "Content-Type: application/vnd.api+json" \ --data-binary '{"data":{"type":"upload_request","attributes":{"filename":"image.png"}}}'await fetch("https://site-api.datocms.com/upload-requests", { method: "POST", headers: { Authorization: "Bearer YOUR-API-TOKEN", Accept: "application/json", "X-Api-Version": "3", "Content-Type": "application/vnd.api+json", }, body: JSON.stringify({ data: { type: "upload_request", attributes: { filename: "image.png" } }, }),});HTTP/1.1 202 AcceptedContent-Type: application/jsonCache-Control: cache-control: max-age=0, private, must-revalidateX-RateLimit-Limit: 30X-RateLimit-Remaining: 28
{ "data": { "id": "/160228/1745427779-image.png", "type": "upload_request", "attributes": { "url": "https://datocms-assets.6c36efb897e5eae1d2a887cfa632eea9.eu.r2.cloudflarestorage.com/160228/1745427779-image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=blahblah...", "request_headers": { "X-Addition-Header-To-Add-In-Step-2": "some-value" } } }}With the presigned bucket URL from step 1, you now have the information you need in order to upload the actual file to the bucket. This uploads the file from your computer to the bucket storage (for regular users, it is Cloudflare R2, but Enterprise plans can specify different options).
Requirements:
- The HTTP method MUST be a
PUTto the bucket URL. - The signed bucket URL is the
data.attributes.urlproperty of the previous step's JSON response. - Each header specified in the
data.attributes.request_headersproperty of the previous step's JSON response MUST be set as an HTTP request header in this request. Although this property is often empty, it sometimes contains essential information for certain buckets (like enterprise assets to be saved to Azure Blob Storage). - You should always add the proper
Content-Typeheader, likeContent-Type: application/pdf. This is especially important for any uploads that are NOT images, as for those, that's the only way we know the format of the file. - The content of the file must be inserted as the raw binary body of the request (no multipart upload).
Experiencing a Missing x-amz-content-sha256 error? This issue occurs if an HTTP Authorization header is included in your request. The URL data.attributes.url already has a signature, so adding an Authorization header will confuse the endpoint. Remove the header, which may be there due to code reuse or inherited authentication in Postman, and try again.
PUT https://datocms-assets.6c36efb897e5eae1d2a887cfa632eea9.eu.r2.cloudflarestorage.com/160228/1745427779-image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=blahblah... HTTP/1.1Content-Type: image/pngX-Addition-Header-To-Add-In-Step-2: some-value
<YOUR_FILE_BINARY_CONTENT>curl -g 'https://datocms-assets.6c36efb897e5eae1d2a887cfa632eea9.eu.r2.cloudflarestorage.com/160228/1745427779-image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=blahblah...' \ -X PUT \ -H "Content-Type: image/png" \ -H "X-Addition-Header-To-Add-In-Step-2: some-value" \ --data-binary '<YOUR_FILE_BINARY_CONTENT>'await fetch( "https://datocms-assets.6c36efb897e5eae1d2a887cfa632eea9.eu.r2.cloudflarestorage.com/160228/1745427779-image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=blahblah...", { method: "PUT", headers: { "Content-Type": "image/png", "X-Addition-Header-To-Add-In-Step-2": "some-value", }, body: "<YOUR_FILE_BINARY_CONTENT>", },);HTTP/1.1 202 AcceptedServer: cloudflare
{ "data": { "type": "job", "id": "4235" }}This step initiates a background job to associate your uploaded file with a new asset in DatoCMS.
Background jobs typically complete in 3-5 seconds but may take longer under heavy load. Once the background job is completed, the asset will automatically appear in your DatoCMS media area.
To start the background job, make a POST request to the /uploads endpoint with a JSON body like the example below. In your request, data.attributes.path should be the data.id from Step 1's response.
The response will include a job ID indicating that the creation process has started. Use this ID in Step 4 to check the job completion status and any potential errors.
Notes:
-
If you choose to provide default field metadata, the
alt,title, andcustom_datafields are ALL required. -
Validation errors will not be immediately returned in this step. You must perform the next step (retrieve the job result) to see any errors that might have occurred.
-
To improve batch upload throughput, submit multiple files in parallel rather than sequentially. Once you receive a 202 response with a job ID for one upload, immediately submit another file without waiting for the first to complete. This significantly reduces total upload time: just remember to track each job ID separately to check individual completion status and any errors.
POST https://site-api.datocms.com/uploads HTTP/1.1Authorization: Bearer YOUR-API-TOKENAccept: application/jsonX-Api-Version: 3Content-Type: application/vnd.api+json
{ "data": { "type": "upload", "attributes": { "path": "/160228/1745427779-image.png", "author": "Mark", "copyright": "2020 DatoCMS", "default_field_metadata": { "en": { "alt": "Nyan the cat", "title": "My cat", "custom_data": {} } } } }}curl -g 'https://site-api.datocms.com/uploads' \ -X POST \ -H "Authorization: Bearer YOUR-API-TOKEN" \ -H "Accept: application/json" \ -H "X-Api-Version: 3" \ -H "Content-Type: application/vnd.api+json" \ --data-binary '{"data":{"type":"upload","attributes":{"path":"/160228/1745427779-image.png","author":"Mark","copyright":"2020 DatoCMS","default_field_metadata":{"en":{"alt":"Nyan the cat","title":"My cat","custom_data":{}}}}}}'await fetch("https://site-api.datocms.com/uploads", { method: "POST", headers: { Authorization: "Bearer YOUR-API-TOKEN", Accept: "application/json", "X-Api-Version": "3", "Content-Type": "application/vnd.api+json", }, body: JSON.stringify({ data: { type: "upload", attributes: { path: "/160228/1745427779-image.png", author: "Mark", copyright: "2020 DatoCMS", default_field_metadata: { en: { alt: "Nyan the cat", title: "My cat", custom_data: {} }, }, }, }, }),});HTTP/1.1 202 AcceptedContent-Type: application/jsonCache-Control: cache-control: max-age=0, private, must-revalidateX-RateLimit-Limit: 30X-RateLimit-Remaining: 28
{ "data": { "type": "job", "id": "facf9248be977002c9bae231" }}This is the final step in the upload creation lifecycle.
Whereas the previous step started the background job, this endpoint tells you the real-time status of that job. This is how you verify the success or failure of an upload.
Although technically an optional step, we still recommend querying this endpoint a few times in order to keep track of the upload lifecycle. You should keep polling until it either completely succeeds or fails early.
This endpoint tells you several useful things:
- While the job is still processing, the endpoint will return a
404status code. Keep querying once every second or two until you either get a success or failure state. - If the job finishes successfully (i.e., the S3-like upload was successfully associated with a DatoCMS asset), you will get back a
200status code and the successfully associated upload object in thepayload. At this point, the asset will be available in the DatoCMS media area. - If the job fails due to a validation or other error, you will immediately get back a
422or similar error code. Checkpayload.data[]for any objects oftype: "api_error", and itsattributes.details.codeandattributes.details.messagewill tell you specifically what failed. - Once the job either succeeds or fails, the job result will persist for approximately 15 minutes. After that, it will
404again.
On validation failure
This is an example 422 validation failure response:
{ "data": { "id": "c628b8ccf485a127775c635a", "type": "job_result", "attributes": { "payload": { "data": [ { "id": "6a1131", "type": "api_error", "attributes": { "code": "INVALID_FIELD", "details": { "field": "default_field_metadata.en", "code": "INVALID_FORMAT", "message": "Must contain alt, title and custom_data" }, "doc_url": "https://www.datocms.com/docs/content-management-api/errors#INVALID_FIELD" } } ] }, "status": 422, "statusText": "Unprocessable Entity" } }}Validation errors should be available within a second or two under typical conditions. If you don't get an error, it's safe to assume the job is still processing... just keep polling and you should get a success soon afterward. The whole process usually takes only 3-5 seconds per image, but can be a little slower under heavy load.
On success
A successful response would look like the "HTTP Response" below.
GET https://site-api.datocms.com/job-results/facf9248be977002c9bae231 HTTP/1.1Authorization: Bearer YOUR-API-TOKENAccept: application/jsonX-Api-Version: 3Content-Type: application/vnd.api+jsoncurl -g 'https://site-api.datocms.com/job-results/facf9248be977002c9bae231' \ \ -H "Authorization: Bearer YOUR-API-TOKEN" \ -H "Accept: application/json" \ -H "X-Api-Version: 3" \ -H "Content-Type: application/vnd.api+json"await fetch( "https://site-api.datocms.com/job-results/facf9248be977002c9bae231", { headers: { Authorization: "Bearer YOUR-API-TOKEN", Accept: "application/json", "X-Api-Version": "3", "Content-Type": "application/vnd.api+json", }, },);HTTP/1.1 202 AcceptedContent-Type: application/jsonCache-Control: cache-control: max-age=0, private, must-revalidateX-RateLimit-Limit: 30X-RateLimit-Remaining: 28
{ "data": { "type": "job-result", "id": "facf9248be977002c9bae231", "attributes": { "status": 200, "payload": { "data": { "type": "upload", "id": "666", "attributes": { "size": 444, "width": 30, "height": 30, "path": "/160228/1745427779-image.png", "url": "https://www.datocms-assets.com/160228/1745427779-image.png", "basename": "image", "format": "jpg", "alt": "Nyan the cat", "title": "My cat", "is_image": true, "author": "Mark", "copyright": "2020 DatoCMS", "default_field_metadata": { "en": { "alt": "Nyan the cat", "title": "My cat", "custom_data": {} } } } } } } }}Body parameters
RFC 4122 UUID of upload expressed in URL-safe base64 format
"q0VNpiNQSkG6z0lif_O1zg"
Must be exactly "upload".
Upload path
"/45/1496845848-digital-cats.jpg"
Copyright
"2020 DatoCMS"
Author
"Mark Smith"
Notes
"Nyan the cat"
For each of the project's locales, the default metadata to apply if nothing is specified at record's level.
{
en: {
title: "this is the default title",
alt: "this is the default alternate text",
custom_data: { foo: "bar" },
focal_point: { x: 0.5, y: 0.5 },
},
}
Tags
["cats"]
Upload collection to which the asset belongs
Returns
Returns a Job ID. You can then poll for the completion of the job that will eventually return a resource object of type upload