Skip to content

Upload a video

A job starts from a source: a file you upload, or one at a URL that the job copies. Either way it becomes an upload in your organization, which later jobs can reuse for 7 days without sending it again. After that it is deleted.

POST /v1/uploads
{ "file_name": "video.mov", "content_type": "video/quicktime", "size": 2176298566 }
Field
file_name Shown in the dashboard.
content_type Sent again as Content-Type in each PUT. Default application/octet-stream.
size Bytes. Up to your plan’s maximum file size (402 quota_exceeded above it).
progressive true for a progressive upload.

The answer’s mode says how to send the file. Upload URLs last one hour.

mode: "single". PUT the whole file to url, with the headers given, then complete it:

Terminal window
curl -X PUT "$URL" -H "Content-Type: video/quicktime" --data-binary @video.mov
curl -X POST https://api.iomza.com/v1/uploads/$ID/complete -H "Authorization: Bearer $IOMZA_API_KEY"

mode: "multipart". The file is cut into parts of part_size bytes (the last one shorter), and parts has a URL for each. PUT them, several at a time if you like, and keep the ETag header of each response. Then complete the upload with every part:

POST /v1/uploads/{id}/complete
{ "parts": [{ "part_number": 1, "etag": "\"9b2cf535f27731c974343645a3985328\"" }, { "part_number": 2, "etag": "\"…\"" }] }

If URLs expire before you are done, ask for new ones for the parts still missing:

POST /v1/uploads/{id}/parts
{ "part_numbers": [7, 8] }

A part that fails can be sent again to the same URL. Completing twice is harmless: the second call returns the upload as it is.

Progressive uploads: process while it uploads

Section titled “Progressive uploads: process while it uploads”

With "progressive": true, each part is its own object, readable as soon as it lands, and there is no complete. Create the job as soon as the upload starts (input.upload_id): it is transcoded while the file arrives, so the wait after the last byte is a fraction of a normal job’s.

  • PUT each part with the headers given.
  • Send the first and last parts first (containers keep their header and index there), then the rest in order.
  • The job’s upload task shows what has arrived (bytes / total_bytes).
  • If nothing arrives for 30 minutes, the job fails with upload_incomplete.
  • If the job fails before you finish (the file can’t be read, for example), stop uploading: the rest won’t be used. Watch for the job.failed webhook or check the job.
  • Until it is complete, the upload feeds only its own job; after that, other jobs can use it like any other.

This is how the dashboard uploads.

Instead of uploading, create the job with "input": { "url": "https://…" }. The job copies the video first; its import task shows the progress. When the server accepts byte ranges (S3, Google Cloud Storage, Azure, CDNs and almost any web server), the copy is fast and the video is transcoded while it downloads; otherwise it is copied whole, then processed.

The URL must:

  • be https, public or presigned (a presigned URL must stay valid until the copy ends);
  • reach the file in at most 5 redirects; private and internal addresses are refused;
  • point to a file no larger than your plan’s maximum.

For S3 buckets whose name has dots, use the path-style URL (https://s3.<region>.amazonaws.com/<bucket>/<key>): the virtual-hosted one doesn’t match the bucket’s certificate.

The copy becomes an upload (mode: "url") that later jobs can use by upload_id for 7 days. If it fails, the job ends with source_import_failed and the reason (HTTP 404, timeout, too large…).

To choose audio or subtitle tracks, or to refuse a file early, read what a source contains. Only its header is read: about a second, even for 20 GB.

GET /v1/uploads/{id}/tracks
POST /v1/probe { "url": "https://…" }
{
"object": "source_tracks",
"duration": 7216.8,
"bitrate": 21129954,
"video": { "codec": "hevc", "width": 3840, "height": 1600, "frame_rate": 24, "interlaced": false, "hdr": "pq", "unsupported": null },
"audio_tracks": [
{ "index": 0, "language": "spa", "title": null, "codec": "eac3", "channels": 8, "default": true, "atmos": false, "bitrate": 768000 },
{ "index": 1, "language": "eng", "title": null, "codec": "eac3", "channels": 6, "default": false, "atmos": true, "bitrate": 640000 }
],
"subtitle_tracks": [
{ "index": 0, "language": "spa", "title": null, "codec": "subrip", "default": true, "forced": true, "convertible": true }
]
}
  • video.hdr: pq (HDR10 and Dolby Vision) or hlg. HDR sources are tone mapped to SDR, which every screen shows correctly.
  • video.unsupported: a video we can’t process, so you can refuse it before anyone waits for a job. Today the one case is dolby_vision_no_compatible_layer: Dolby Vision profiles 5 and 20, whose colours can’t be reproduced faithfully. Export those as HDR10, HLG, SDR or Dolby Vision profile 8.
  • subtitle_tracks[].convertible: text subtitles (SRT, ASS, WebVTT, mov_text…) become WebVTT; image ones (PGS, VobSub) can’t.

Any common container and codec: MP4, MOV, MKV, WebM, AVI, MPEG-TS, MXF…, with H.264, HEVC, ProRes, VP9, AV1, MPEG-2 and others. Interlaced video is deinterlaced, variable frame rates are made steady, rotation from phones is applied, and anamorphic pixels are corrected: the job’s adjustments say what was done.