Upload files
Start an upload, send the file directly with its link, then finish the upload so the file is stored.
An upload has three steps. The file itself goes straight to storage, never through the API.
Start the upload
Tell us the file’s key, its size in bytes, and its type:
curl -X POST "$STORAGE_URL/buckets/invoices/uploads" \
-H "Authorization: Bearer $STORAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"key": "2026/0001.pdf", "size": 48213, "contentType": "application/pdf"}'The answer:
{
"data": {
"id": "01m3c3zq7rj2ab0x5w8k1v9yag",
"bucket": "01m3c081d2wj4g63qcr53pa1yh",
"key": "2026/0001.pdf",
"size": 48213,
"contentType": "application/pdf",
"expiresAt": "2026-09-26T11:00:00Z",
"upload": {
"method": "PUT",
"url": "https://…",
"headers": {
"Content-Type": "application/pdf",
"X-Amz-Meta-Key": "2026%2F0001.pdf"
},
"expiresAt": "2026-09-26T10:15:00Z"
}
}
}We check the file fits before you send it. A project that is full gets storage_quota_reached, and a file over the size limit gets validation_failed. See Limits.
Send the file
Send the file’s bytes to upload.url, with upload.method, and with every header in upload.headers exactly as given. A missing or changed header makes the link refuse the file.
const response = await fetch(upload.url, {
method: upload.method,
headers: upload.headers,
body: file,
})
if (!response.ok) throw new Error(`The upload was refused: ${response.status}`)The link works for 15 minutes. This works from a browser too, so your users’ files can go straight to storage from their device.
Finish the upload
Once the file arrives, we finish the upload for you, usually within seconds, and call your webhook if the bucket has one. Finish it yourself when you want the stored file in the answer straight away:
curl -X POST "$STORAGE_URL/buckets/invoices/uploads/$UPLOAD_ID/complete" \
-H "Authorization: Bearer $STORAGE_API_KEY"The answer is the stored file. Until you finish, the file is not listed or downloadable.
- Finishing twice is safe. The second time answers with the same file.
- Finishing before the file has arrived gets
upload_incomplete. Send the file, then try again. - A file larger than the size you declared is refused with
upload_size_mismatch, and the upload is cancelled. Start a new one. - If the file never arrives, the upload is cancelled after an hour.
To cancel an upload yourself, delete it:
curl -X DELETE "$STORAGE_URL/buckets/invoices/uploads/$UPLOAD_ID" \
-H "Authorization: Bearer $STORAGE_API_KEY"Check the file arrived whole
Send the file’s SHA-256 digest, as 64 hexadecimal characters, when you start the upload:
DIGEST=$(shasum -a 256 0001.pdf | cut -d ' ' -f 1)
-d "{
\"key\": \"2026/0001.pdf\",
\"size\": 48213,
\"contentType\": \"application/pdf\",
\"sha256\": \"$DIGEST\"
}"The link then carries the digest in its headers, and a file whose bytes do not match is refused. The stored file keeps the digest in sha256.
Replace a file
Upload to the same key. When the new upload finishes, the key points to the new file, and the old one is removed. People downloading the file at that moment get either the old file or the new one.
Last updated