Start a Direct Blob Upload
curl --request POST \
--url https://blob.vertracloud.app/v1/uploads \
--header 'Content-Type: application/json' \
--data '
{
"path": "<string>",
"size": 123,
"visibility": "<string>",
"overwrite": true,
"rename_on_conflict": true,
"random_suffix": true,
"cache_control": "<string>",
"content_disposition": "<string>",
"filename": "<string>",
"expires_at": "<string>",
"checksum_sha256": "<string>",
"metadata": {}
}
'import requests
url = "https://blob.vertracloud.app/v1/uploads"
payload = {
"path": "<string>",
"size": 123,
"visibility": "<string>",
"overwrite": True,
"rename_on_conflict": True,
"random_suffix": True,
"cache_control": "<string>",
"content_disposition": "<string>",
"filename": "<string>",
"expires_at": "<string>",
"checksum_sha256": "<string>",
"metadata": {}
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
path: '<string>',
size: 123,
visibility: '<string>',
overwrite: true,
rename_on_conflict: true,
random_suffix: true,
cache_control: '<string>',
content_disposition: '<string>',
filename: '<string>',
expires_at: '<string>',
checksum_sha256: '<string>',
metadata: {}
})
};
fetch('https://blob.vertracloud.app/v1/uploads', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://blob.vertracloud.app/v1/uploads",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'path' => '<string>',
'size' => 123,
'visibility' => '<string>',
'overwrite' => true,
'rename_on_conflict' => true,
'random_suffix' => true,
'cache_control' => '<string>',
'content_disposition' => '<string>',
'filename' => '<string>',
'expires_at' => '<string>',
'checksum_sha256' => '<string>',
'metadata' => [
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://blob.vertracloud.app/v1/uploads"
payload := strings.NewReader("{\n \"path\": \"<string>\",\n \"size\": 123,\n \"visibility\": \"<string>\",\n \"overwrite\": true,\n \"rename_on_conflict\": true,\n \"random_suffix\": true,\n \"cache_control\": \"<string>\",\n \"content_disposition\": \"<string>\",\n \"filename\": \"<string>\",\n \"expires_at\": \"<string>\",\n \"checksum_sha256\": \"<string>\",\n \"metadata\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://blob.vertracloud.app/v1/uploads")
.header("Content-Type", "application/json")
.body("{\n \"path\": \"<string>\",\n \"size\": 123,\n \"visibility\": \"<string>\",\n \"overwrite\": true,\n \"rename_on_conflict\": true,\n \"random_suffix\": true,\n \"cache_control\": \"<string>\",\n \"content_disposition\": \"<string>\",\n \"filename\": \"<string>\",\n \"expires_at\": \"<string>\",\n \"checksum_sha256\": \"<string>\",\n \"metadata\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://blob.vertracloud.app/v1/uploads")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"path\": \"<string>\",\n \"size\": 123,\n \"visibility\": \"<string>\",\n \"overwrite\": true,\n \"rename_on_conflict\": true,\n \"random_suffix\": true,\n \"cache_control\": \"<string>\",\n \"content_disposition\": \"<string>\",\n \"filename\": \"<string>\",\n \"expires_at\": \"<string>\",\n \"checksum_sha256\": \"<string>\",\n \"metadata\": {}\n}"
response = http.request(request)
puts response.read_body{
"response": {
"id": "7c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
"path": "videos/demo.mp4",
"mode": "multipart",
"status": "pending",
"size": 2147483648,
"expires_at": "2026-10-14T13:00:00.000Z",
"headers": {
"content-type": "video/mp4"
},
"part_size": 67108864,
"part_count": 32,
"uploaded_parts": [],
"created_at": "2026-10-14T12:00:00.000Z"
}
}
Uploads
Start a Direct Blob Upload
Starts an upload of up to 10 GB that goes straight to storage. Returns the URLs to send the bytes to. Accepts an upload token.
POST
/
v1
/
uploads
Start a Direct Blob Upload
curl --request POST \
--url https://blob.vertracloud.app/v1/uploads \
--header 'Content-Type: application/json' \
--data '
{
"path": "<string>",
"size": 123,
"visibility": "<string>",
"overwrite": true,
"rename_on_conflict": true,
"random_suffix": true,
"cache_control": "<string>",
"content_disposition": "<string>",
"filename": "<string>",
"expires_at": "<string>",
"checksum_sha256": "<string>",
"metadata": {}
}
'import requests
url = "https://blob.vertracloud.app/v1/uploads"
payload = {
"path": "<string>",
"size": 123,
"visibility": "<string>",
"overwrite": True,
"rename_on_conflict": True,
"random_suffix": True,
"cache_control": "<string>",
"content_disposition": "<string>",
"filename": "<string>",
"expires_at": "<string>",
"checksum_sha256": "<string>",
"metadata": {}
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
path: '<string>',
size: 123,
visibility: '<string>',
overwrite: true,
rename_on_conflict: true,
random_suffix: true,
cache_control: '<string>',
content_disposition: '<string>',
filename: '<string>',
expires_at: '<string>',
checksum_sha256: '<string>',
metadata: {}
})
};
fetch('https://blob.vertracloud.app/v1/uploads', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://blob.vertracloud.app/v1/uploads",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'path' => '<string>',
'size' => 123,
'visibility' => '<string>',
'overwrite' => true,
'rename_on_conflict' => true,
'random_suffix' => true,
'cache_control' => '<string>',
'content_disposition' => '<string>',
'filename' => '<string>',
'expires_at' => '<string>',
'checksum_sha256' => '<string>',
'metadata' => [
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://blob.vertracloud.app/v1/uploads"
payload := strings.NewReader("{\n \"path\": \"<string>\",\n \"size\": 123,\n \"visibility\": \"<string>\",\n \"overwrite\": true,\n \"rename_on_conflict\": true,\n \"random_suffix\": true,\n \"cache_control\": \"<string>\",\n \"content_disposition\": \"<string>\",\n \"filename\": \"<string>\",\n \"expires_at\": \"<string>\",\n \"checksum_sha256\": \"<string>\",\n \"metadata\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://blob.vertracloud.app/v1/uploads")
.header("Content-Type", "application/json")
.body("{\n \"path\": \"<string>\",\n \"size\": 123,\n \"visibility\": \"<string>\",\n \"overwrite\": true,\n \"rename_on_conflict\": true,\n \"random_suffix\": true,\n \"cache_control\": \"<string>\",\n \"content_disposition\": \"<string>\",\n \"filename\": \"<string>\",\n \"expires_at\": \"<string>\",\n \"checksum_sha256\": \"<string>\",\n \"metadata\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://blob.vertracloud.app/v1/uploads")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"path\": \"<string>\",\n \"size\": 123,\n \"visibility\": \"<string>\",\n \"overwrite\": true,\n \"rename_on_conflict\": true,\n \"random_suffix\": true,\n \"cache_control\": \"<string>\",\n \"content_disposition\": \"<string>\",\n \"filename\": \"<string>\",\n \"expires_at\": \"<string>\",\n \"checksum_sha256\": \"<string>\",\n \"metadata\": {}\n}"
response = http.request(request)
puts response.read_body{
"response": {
"id": "7c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
"path": "videos/demo.mp4",
"mode": "multipart",
"status": "pending",
"size": 2147483648,
"expires_at": "2026-10-14T13:00:00.000Z",
"headers": {
"content-type": "video/mp4"
},
"part_size": 67108864,
"part_count": 32,
"uploaded_parts": [],
"created_at": "2026-10-14T12:00:00.000Z"
}
}
Starts a direct upload: the bytes go from your client straight to storage, not through the API. Files up to the plan’s single-part limit come back in
single mode with one url. Larger files come back in multipart mode with a part size and count. Then send the bytes and call Complete a direct upload. The reserved storage counts toward the quota until the upload completes, aborts or expires.
Scope: blob:write. Accepts an upload token.
Upload token: this route also accepts an upload token (Authorization: Bearer vcup_...) instead of an API key.
string
required
Destination path, with the same rules as Upload in one request.
number
required
Exact size of the file in bytes, from 0 up to 10 GB. The stored bytes must match it.
string
public or private. Default private.boolean
Replace an object already at
path. Default false.boolean
When
path is taken, save the file as name-1.ext, name-2.ext and so on (the first free name) instead of returning BLOB_OBJECT_ALREADY_EXISTS. The upload carries the final path. Default false. Can’t be combined with overwrite, and has no effect with an upload token that fixes the path.boolean
Insert a random suffix before the extension. Default
false; forced to true for an upload token without a fixed path.string
immutable, max-age=<seconds> or no-cache (Enterprise-26 or higher).string
inline or attachment. HTML, SVG, XML, XHTML and HTM are always attachment.string
Name suggested on download.
string
ISO 8601 timestamp for deletion, at least the plan’s minimum from now.
string
Hex SHA-256 of the whole file. Checked when the upload completes. Only valid for a direct upload sent in a single part; a multipart upload refuses it with
VALIDATION_ERROR.object
Up to 5 keys, each
[a-z0-9-]{1,64}, 512 bytes in total. Enterprise-12 or higher.string
The workspace ID, when the blob belongs to a workspace. The call acts on the blob of the workspace owner and needs the
blob:read workspace permission (blob:manage on routes that change data). Leave it out to act on your own blob.Example request
curl -X POST "https://blob.vertracloud.app/v1/uploads" \
-H "Authorization: Bearer $VERTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"path":"videos/demo.mp4","size":2147483648,"visibility":"private"}'
Response
object
The upload, with the URLs to send the bytes to.
Show properties
Show properties
string
Upload ID. Use it for status, parts, complete and abort.
string
Final path, including the random suffix when one was requested.
string
single (one PUT of the whole file to url) or multipart (several parts, each with its own URL).string
pending, completed, aborted or expired.number
Declared size in bytes. The stored bytes must match it at
complete.string
When the URLs and the reserved storage expire, as an ISO 8601 timestamp.
string
Only in
single mode: the URL to PUT the whole file to.object
Headers the
PUT must send exactly as given, because they are part of the signature.number
Only in
multipart mode: the fixed part size in bytes. The last part can be smaller.number
Only in
multipart mode: how many parts the file has.number[]
Part numbers already stored. Use them to resume an interrupted upload.
string
ISO 8601 creation time.
{
"response": {
"id": "7c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
"path": "videos/demo.mp4",
"mode": "multipart",
"status": "pending",
"size": 2147483648,
"expires_at": "2026-10-14T13:00:00.000Z",
"headers": {
"content-type": "video/mp4"
},
"part_size": 67108864,
"part_count": 32,
"uploaded_parts": [],
"created_at": "2026-10-14T12:00:00.000Z"
}
}
Error Responses
string
The error code indicating the reason for failure. Possible values:
AUTH_TOKEN_MISSING(401): NoAuthorizationheader was sent.API_KEY_INVALID(401): The API key doesn’t exist, was revoked, or its secret doesn’t match.API_KEY_SCOPE_DENIED(403): The key lacks the scope this route needs (blob:readorblob:write);details.requirednames it.WORKSPACE_PERMISSION_DENIED(403): Only withworkspace_id: you’re not a member, or your role lacks the blob permission.WORKSPACE_NOT_FOUND(404): Only withworkspace_id: no workspace with that ID, or you’re not a member.BLOB_UPLOAD_TOKEN_INVALID(401): The upload token doesn’t exist, has expired or was revoked (only with an upload token).BLOB_NAMESPACE_SUSPENDED(403): The account’s blob storage is suspended, or its plan expired. Uploads and other writes are refused from the start of the 72-hour grace period.VALIDATION_ERROR(400): A parameter or body field failed validation.details.pathnames the field.BLOB_UPLOAD_TOKEN_CONSTRAINT(403): Only with an upload token: the path, folder or another option the token fixes doesn’t match the request.PLAN_RESTRICTED_FEATURE(403):metadata(below Enterprise-12) orcache_control: no-cache(below Enterprise-26) isn’t included in the plan.BLOB_UPLOAD_LIMIT(409): The account already has as many uploads in progress as its plan allows (max_open_uploads).BLOB_OBJECT_ALREADY_EXISTS(409):pathis taken andoverwriteis nottrue.BLOB_FILE_TOO_LARGE(413):sizeis above 10 GB, the folder rule’smax_size, or the token’smax_size.STORAGE_QUOTA_EXCEEDED(403): The reserved size plus what’s already used would exceed the plan’s blob quota.BLOCKED_FILE_TYPE(422): The extension is blocked or not allowed by the folder rule or token.BLOB_UPLOAD_TOKEN_EXHAUSTED(401): Only with an upload token: it has used all its uses.RATE_LIMIT_EXCEEDED(429): Below Enterprise-12, uploads are limited to 1 per second.details.retry_aftergives the wait.
{
"code": "AUTH_TOKEN_MISSING"
}
{
"code": "API_KEY_SCOPE_DENIED"
}
{
"code": "WORKSPACE_NOT_FOUND"
}
{
"code": "VALIDATION_ERROR"
}
{
"code": "BLOB_UPLOAD_LIMIT"
}
{
"code": "BLOB_FILE_TOO_LARGE"
}
{
"code": "BLOCKED_FILE_TYPE"
}
{
"code": "RATE_LIMIT_EXCEEDED"
}