Developers

Uploading Files Through the API

The three-step upload protocol: presign, PUT the bytes to storage, then complete. Limits, conflicts, and how to retry.

Advanced, 10 minUpdated October 2026

Read firstAuthenticating with an API Key

Files never pass through the API server. Your client asks for a signed storage URL, sends the bytes straight to storage, then tells ReelStorage the upload is done. Three calls per file, batchable at the first step. The key needs the uploadAssets scope, and createFolders if it will make folders as it goes.

What You'll Learn

  • The presign → PUT → complete sequence
  • Which limits apply at each step
  • How name conflicts are reported and resolved
  • What to retry, and what not to

Step 1: Presign

POST /api/v1/projects/{projectId}/uploads with up to 100 files. Each entry needs the filename, the exact byte size, and the MIME type. folderId is optional.

{
  "files": [
    { "filename": "shot_001.jpg", "fileSize": 123456, "mimeType": "image/jpeg" },
    { "filename": "shot_002.jpg", "fileSize": 234567, "mimeType": "image/jpeg", "folderId": "FOLDER_ID" }
  ]
}

The response is positional: items[0] answers files[0].

{
  "items": [
    {
      "ok": true,
      "assetId": "ASSET_ID",
      "uploadKey": "…",
      "uploadUrl": "https://…",
      "filename": "shot_001",
      "originalFilename": "shot_001.jpg",
      "wasRenamed": false,
      "expiresIn": 1800
    },
    {
      "ok": false,
      "code": "CONFLICT",
      "error": "A file with this name and extension exists",
      "existingAssetId": "…"
    }
  ]
}

Keep everything from a successful item; step 3 needs it. filename is the display name ReelStorage allocated. Filenames are unique within a project, so a duplicate comes back either as a CONFLICT item or, when a suffix resolves it, with wasRenamed: true and a name like shot_001-1.

A per-item failure carries FILE_TOO_LARGE, UNSUPPORTED_TYPE, CONFLICT, STORAGE_LIMIT_EXCEEDED, UPLOAD_RESTRICTED, or REJECTED. The files in a batch count against the storage limit together, in order. A file that would take the batch past the limit fails with STORAGE_LIMIT_EXCEEDED; a smaller file after it can still fit. When every file fails the same way, the response is a top-level 422 or 403 instead; see Errors.

Step 2: PUT the Bytes

One PUT to uploadUrl with the file as the body. Set Content-Type to the mimeType you presigned and nothing else. The body length must equal fileSize.

curl --request PUT "$UPLOAD_URL" \
  --header "Content-Type: image/jpeg" \
  --data-binary @shot_001.jpg
  • The URL is valid for 30 minutes (expiresIn). If it expires mid-transfer, re-run step 1 for that one file and PUT again.
  • Single files up to 5 GiB.
  • No API key on this request. The URL is the credential.

Step 3: Complete

POST /api/v1/projects/{projectId}/uploads/complete once the PUT returns 200. Echo the presign response back, with finalFilename set to the filename from step 1.

{
  "assetId": "ASSET_ID",
  "uploadKey": "…",
  "finalFilename": "shot_001",
  "originalFilename": "shot_001.jpg",
  "fileSize": 123456,
  "mimeType": "image/jpeg",
  "folderId": "FOLDER_ID"
}

To tag the new asset, call the tag-asset route afterwards.

ReelStorage checks that the upload arrived, that its size matches, and that it still fits the storage limit, then creates the asset and returns 201. If the limit filled up after presign, the uploaded file is deleted and complete returns 422 STORAGE_LIMIT_EXCEEDED. Processing (thumbnails, metadata extraction, video renditions) runs afterwards; the asset's status moves from pending to ready and you can poll the get-asset route if you need to know when.

Warning

Complete is not idempotent. A second complete for the same assetId returns 409 CONFLICT. Record completion on your side before retrying anything.

Retry Rules

SituationDo
Presign item CONFLICTRename on your side and presign again, or skip; existingAssetId tells you what is already there
PUT fails or times outPUT again to the same URL if it is still within 30 minutes; otherwise presign that file again
Complete returns 409The asset already exists; do not presign again
Complete returns 400 with field errorsYour echoed fields do not match the presign; fix and resend
STORAGE_LIMIT_EXCEEDED, on an item, the batch, or completeStop uploading; storage must be freed or the plan raised in the app

What's Next

Tell us what you're shooting next.

Start with one title. Upload a shoot, bring its metadata in from a CSV, and confirm the cast. Then decide.

A person on our team reads it and writes back.