Large-file uploads
Resumable, integrity-checked multipart uploads for multi-GB datasets — films, VFX caches, simulation bundles, and model weights. Why there is no 25 MB ceiling.
Type: How-to · Audience: Pipeline TDs, researchers, automation builders
KinoCloud is built for heavy media: film exports, VFX caches, simulation bundles, molecular datasets, and AI model weights routinely run from a few hundred MB to tens of GB. To carry those at scale the API follows one rule:
Large user data never travels through an API request or response body. The API issues upload intents (presigned URLs) and job references; object storage carries the bytes.
This is why there is no 25 MB-class limit on what you can upload. The 25 MB ceiling people hit elsewhere is the limit of model/transcription endpoints that accept raw file bytes in the request body. KinoCloud never does that — your bytes go directly from your client to object storage over a presigned URL, and the job carries only the resulting storage key.
Which upload method to use
| Method | Endpoint | Use when |
|---|---|---|
| Single-PUT | POST /v1/jobs/upload-url |
Small/medium assets (≤ 5 GiB) where a one-shot upload is fine |
| Resumable multipart | POST /v1/uploads |
Large datasets, flaky networks, or anything you want to upload in parallel and resume |
A single presigned PUT cannot exceed the provider's single-object limit (S3 5 GiB, Azure 5000 MiB) and cannot resume — a dropped connection restarts from byte 0. For real film/VFX/model payloads, use multipart.
Resumable multipart flow
init → POST /v1/uploads → { uploadId, parts[], partSize }
parts → PUT each part directly to its URL → collect ETags (S3)
status→ GET /v1/uploads/{uploadId} (resume) → { received[], missing[] }
done → POST /v1/uploads/{uploadId}/complete → { key, verified: true }
submit→ POST /v1/jobs/submit { sceneFile: key }
1. Initiate
// POST /v1/uploads
{
"filename": "feature_cut.mov",
"contentType": "video/quicktime",
"size": 18253611008,
"sha256": "9f86d0818…" // optional, verified at completion
}
The response presigns the first page of part URLs (at most 1,000 inline):
{
"uploadId": "eyJrIjoidXBsb2Fkcy8…", // opaque handle for later calls
"key": "uploads/<userId>/<group>/feature_cut.mov",
"partSize": 67108864,
"partCount": 272,
"parts": [{ "partNumber": 1, "url": "https://…" }, …],
"expiresIn": 3600,
"provider": "aws",
"checksumAlgorithm": null
}
Add "checksumAlgorithm": "sha256" to the init body (S3 deployments) to make
the store itself verify every part: no inline URLs are returned, and each
URL is minted with that part's checksum instead (next step). The default part
plan scales the part size so even a 200 GiB file stays around 1,000 parts.
2. Upload the parts
PUT each part's bytes to its url — in parallel, with your own retry. On S3,
capture the ETag response header for each part. Parts may be uploaded in any
order.
Need URLs beyond the inline page, a replacement for an expired URL, or checksum-signed URLs? Mint them on demand:
// POST /v1/uploads/{uploadId}/parts
{ "parts": [{ "partNumber": 7, "checksumSha256": "<base64 SHA-256 of part 7>" }] }
// → { "parts": [{ "partNumber": 7, "url": "https://…",
// "headers": { "x-amz-checksum-sha256": "<same base64>" } }], … }
Send each returned headers entry with that part's PUT. checksumSha256 is
required per part for checksum-mode uploads (and rejected otherwise); with it,
S3 refuses any part body that doesn't hash to exactly that value.
3. Resume (if interrupted)
GET /v1/uploads/{uploadId}
→ { "received": [{ "partNumber": 1, "etag": "…" }], "missing": [2,3,…], "complete": false }
Re-PUT only the missing parts. State lives in the store, so a crashed client
resumes without re-uploading finished parts.
4. Complete (integrity gate)
// POST /v1/uploads/{uploadId}/complete
{ "parts": [{ "partNumber": 1, "etag": "\"abc…\"" }, …] }
Checksum-mode uploads must also echo each part's checksumSha256 — the store
re-validates every value against what it recorded when the part landed.
Completion is fail-closed: an empty or size-mismatched assembly returns
4xx and the object is not reported verified. On success you get the
submittable key:
{
"key": "uploads/<userId>/<group>/feature_cut.mov",
"size": 18253611008,
"verified": true,
"integrity": "size" // "parts-sha256" when the store verified every part's bytes
}
5. Submit
Pass the verified key straight through as sceneFile:
// POST /v1/jobs/submit
{ "jobType": "render", "sceneFile": "uploads/<userId>/<group>/feature_cut.mov", "frameStart": 1, "frameEnd": 250, "outputFormat": "EXR" }
Abort
DELETE /v1/uploads/{uploadId} releases staged parts. Abandoned uploads are
also swept automatically by retention, so you are never billed for half-finished
multipart parts indefinitely.
Downloads
Job outputs are returned the same way — GET /v1/jobs/{id}/outputs gives you
short-lived presigned download URLs (plus size and etag per file), so
output bytes flow client↔storage and never through the API. Download large
outputs in parallel with HTTP range requests.
SDK / CLI
The MCP client (@kinocloud/mcp) wraps the whole flow:
const { uploadId, parts } = await client.initUpload({ filename, contentType, size });
// PUT each parts[i].url …
const status = await client.getUploadStatus(uploadId); // resume
await client.completeUpload(uploadId, completedParts);