Upload a video file
Upload the file straight to storage, in one part or in several, and get the video_id to clip.
Use the upload when the video is with you and not at a public link. The file goes straight from your server to storage, through a signed URL, and does not pass through the API. At the end you have a video_id identical to the one from POST /clips/info.
Accepted formats: .mp4, .mov, .webm and .mkv. The maximum size comes from the plan.
Request the upload URL
curl -X POST https://api.cut.pro/api/v1/videos/upload \
-H "X-Api-Key: $CUTPRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "file_name": "podcast-ep-42.mp4", "file_size": 734003200, "content_type": "video/mp4" }'{
"mode": "single",
"video_id": "7412911002237764",
"upload_url": "https://...",
"expires_in": 3600
}mode says how to send: single (one PUT) or multipart (several parts). A file that is too large returns 400 FILE_TOO_LARGE with the ceiling in extra.max_size.
Send the bytes
With mode: "single", one PUT with the same Content-Type you declared:
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
--upload-file podcast-ep-42.mp4With mode: "multipart", see Large files.
Register the video
After the PUT responded 200, report the duration, width and height:
curl -X POST https://api.cut.pro/api/v1/videos/upload/complete \
-H "X-Api-Key: $CUTPRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_id": "7412911002237764",
"file_name": "podcast-ep-42.mp4",
"duration": 3712,
"width": 1920,
"height": 1080
}'The response has the same format as POST /clips/info, with credits_cost and current_balance. Send the id in POST /clips and continue as in Clip a video.
Large files
When the file exceeds the limit of a single PUT, POST /videos/upload responds mode: "multipart":
{
"mode": "multipart",
"video_id": "7412911002237764",
"upload_id": "2~kB1x...",
"part_urls": ["https://...", "https://...", "https://..."],
"part_size": 104857600,
"expires_in": 3600
}- Cut the file into pieces of
part_sizebytes. Only the last one can be smaller. - Make a PUT for each piece to the URL in the same position in
part_urls. Position 0 is part 1. - Keep the
ETagheader from each response. - Close the upload with the parts in order:
curl -X POST https://api.cut.pro/api/v1/videos/upload/multipart/complete \
-H "X-Api-Key: $CUTPRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_id": "7412911002237764",
"upload_id": "2~kB1x...",
"parts": [
{ "part_number": 1, "etag": "\"a54357aff0632cce46d942af68356b38\"" },
{ "part_number": 2, "etag": "\"0c78aef83f66abc1fa1e8477f296d394\"" },
{ "part_number": 3, "etag": "\"acbd18db4cc2f85cedef654fccc4a4d8\"" }
]
}'- Register with
POST /videos/upload/complete, as in step 3 above.
import { open } from "node:fs/promises";
const file = await open("podcast-ep-42.mp4");
const parts = [];
for (const [index, url] of upload.part_urls.entries()) {
const buffer = Buffer.alloc(upload.part_size);
const { bytesRead } = await file.read(buffer, 0, upload.part_size, index * upload.part_size);
const response = await fetch(url, { method: "PUT", body: buffer.subarray(0, bytesRead) });
parts.push({ part_number: index + 1, etag: response.headers.get("ETag") });
}
await file.close();Gave up halfway? Call POST /videos/upload/multipart/abort with video_id and upload_id. Without it, the parts already sent keep taking up storage.
Errors
| Code | When |
|---|---|
INVALID_FILE_TYPE | Extension outside the list. extra.allowed_extensions carries the accepted ones. |
FILE_TOO_LARGE | Exceeds the plan ceiling, reported in extra.max_size. |
FILE_NOT_FOUND | The complete call arrived before the file was in storage. Check the PUT. |
VIDEO_TOO_LONG, VIDEO_TOO_SHORT | Duration outside what is allowed. extra.duration and extra.max_duration say by how much. |