Transcoding API: Convert Video at Scale Into Your Own Bucket
The Transcoding API turns one source video into the renditions you actually ship (MP4 files at several resolutions, an HLS adaptive ladder, thumbnail sprites, preview GIFs) and writes them straight into your own S3-compatible bucket. You hand us an input and a destination; we read, encode, and deliver. We never keep your media.
It's the standard pattern for VOD platforms, user-generated-content pipelines, course/webinar processing, social previews, and any case where you have a stack of source files and need consistent, multi-format output without running ffmpeg yourself.
How it works (in one paragraph)
You create a job: an input (a public URL or an object in your S3 bucket), an output destination (your S3 bucket + a path prefix), and a list of outputs, the formats you want. We fetch the source, encode every requested rendition in parallel, package the results, and upload them under your destination prefix. The job moves through queued → analyzing → encoding → finalizing → completed, exposing a 0–100 progress number the whole way. When it finishes we can ping a webhook you specify, and the produced files are sitting in your bucket. Your storage credentials are used only for that job and stored encrypted; we never return them and never hold the output ourselves.
Before you start
- Get an API token in the dashboard: my.cubepath.com → Account → API Tokens. Send it on every request as
Authorization: Bearer <token>(orX-API-Key: <token>). - Scopes: reading job status needs
transcoder:read; creating or canceling jobs needstranscoder:write. - Write requests (
POST,DELETE) must also carry the headerX-Requested-With: XMLHttpRequest. - Account state: your organization must be verified, not suspended, and carry a positive balance to create jobs.
- Base URL:
https://api.cubepath.com.
You bring the storage. Both the input (when it's S3) and the output point at any S3-compatible endpoint: CubePath Object Storage, AWS S3, Cloudflare R2, Backblaze B2, Wasabi, MinIO. The
secret_keyyou send is encrypted at rest and is never echoed back in any API response.
Create your first job
A minimal job: take one MP4 and produce a 1080p and a 720p MP4 in your bucket.
cURL
curl -X POST https://api.cubepath.com/transcoder/jobs \
-H "Authorization: Bearer $CUBEPATH_TOKEN" \
-H "X-Requested-With: XMLHttpRequest" \
-H "Content-Type: application/json" \
-d '{
"input": {
"source": "url",
"url": "https://example.com/source/talk.mp4"
},
"output": {
"s3": {
"endpoint": "https://s3.eu-central-1.amazonaws.com",
"region": "eu-central-1",
"bucket": "my-media",
"path": "talks/talk-42/",
"access_key": "AKIA...",
"secret_key": "..."
}
},
"outputs": [
{ "type": "file", "container": "mp4", "codec": "h264", "height": 1080 },
{ "type": "file", "container": "mp4", "codec": "h264", "height": 720 }
]
}'
The response is the created job, including its uuid and status: "queued". Keep the uuid; it's how you poll progress and fetch outputs.
Python
import os, requests
API = "https://api.cubepath.com"
HEADERS = {
"Authorization": f"Bearer {os.environ['CUBEPATH_TOKEN']}",
"X-Requested-With": "XMLHttpRequest",
}
job = requests.post(f"{API}/transcoder/jobs", headers=HEADERS, json={
"input": {"source": "url", "url": "https://example.com/source/talk.mp4"},
"output": {"s3": {
"endpoint": "https://s3.eu-central-1.amazonaws.com",
"region": "eu-central-1", "bucket": "my-media", "path": "talks/talk-42/",
"access_key": os.environ["S3_KEY"], "secret_key": os.environ["S3_SECRET"],
}},
"outputs": [
{"type": "file", "container": "mp4", "codec": "h264", "height": 1080},
{"type": "file", "container": "mp4", "codec": "h264", "height": 720},
],
}).json()
print(job["uuid"], job["status"])
Reading the input from your bucket instead of a URL
Set input.source to "s3" and give the object's location:
"input": {
"source": "s3",
"s3": {
"endpoint": "https://s3.eu-central-1.amazonaws.com",
"region": "eu-central-1",
"bucket": "my-uploads",
"path": "incoming/talk.mp4", // the object key
"access_key": "AKIA...",
"secret_key": "..."
}
}
Output types
Each entry in outputs (1–20 per job) is one format. Mix them freely in a single job; they all read the same source once.
type | Key parameters | Produces |
|---|---|---|
file | codec (h264/h265/vp9/av1), container (mp4/webm/…, default mp4), height (0 = keep source), optional crf, preset, bitrate | output_<i>.<container> (e.g. output_0.mp4) |
hls | ladder: an array of rungs [{ "height": 1080 }, { "height": 720 }, …] (required; no ladder means nothing is produced) | hls<i>/master.m3u8 + per-rung playlists and segments |
thumbnails | interval (seconds between frames) | thumbs<i>/thumb_0001.jpg, thumb_0002.jpg, … |
gif | duration (seconds), optional start ("00:00:05") | output_<i>.gif |
A job that produces an adaptive ladder plus thumbnails and a preview GIF:
"outputs": [
{ "type": "hls", "ladder": [ { "height": 1080 }, { "height": 720 }, { "height": 480 } ] },
{ "type": "thumbnails", "interval": 10 },
{ "type": "gif", "start": "00:00:05", "duration": 3 }
]
Field names matter.
filereadscodec(notvideo_codec).hlsneeds aladder.codecacceptsh264,h265,vp9,av1. Any height works, including2160(4K) and4320(8K). Just remember that 4K/8K and the slower codecs (h265,av1) cost a lot more processing time.
Check status & progress
curl https://api.cubepath.com/transcoder/jobs/<uuid> \
-H "Authorization: Bearer $CUBEPATH_TOKEN"
Key fields in the response:
| Field | Meaning |
|---|---|
status | queued → analyzing → encoding → finalizing → completed (or failed / canceled) |
progress | 0–100 |
total_segments / completed_segments | How much of the encode is done (big files are split and encoded in parallel) |
outputs | Populated when the job completes, with the produced files |
error | The reason, if status is failed |
Poll this endpoint, or set a webhook_url (below) so you don't have to.
Get the outputs
When the job is completed, fetch the produced artifact locations:
curl https://api.cubepath.com/transcoder/jobs/<uuid>/outputs \
-H "Authorization: Bearer $CUBEPATH_TOKEN"
Everything is written under the path prefix you set in output.s3. For an output prefix of talks/talk-42/:
talks/talk-42/output_0.mp4
talks/talk-42/output_1.mp4
talks/talk-42/hls0/master.m3u8
talks/talk-42/thumbs1/thumb_0001.jpg
talks/talk-42/output_2.gif
The files live in your bucket; serve them directly, or put a CDN zone in front. Any intermediate scratch we use during the run is cleaned up automatically after a successful finish.
Webhooks
Add "webhook_url": "https://example.com/hooks/transcode" to a job and we POST JSON when it ends:
// success
{ "job_id": "…", "status": "completed" }
// failure
{ "job_id": "…", "status": "failed", "error": "<reason>" }
Delivery is best-effort and not retried, so treat the webhook as a nudge and keep GET /transcoder/jobs/{uuid} as the source of truth.
Process many files at once
POST /transcoder/jobs/batch creates up to 1000 jobs in one call. They share one output destination and one outputs spec; each input adds an optional out_subpath appended to the output prefix so jobs don't overwrite each other.
{
"output": { "s3": { /* …destination bucket… */ "path": "library/" } },
"outputs": [ { "type": "file", "container": "mp4", "codec": "h264", "height": 720 } ],
"input_defaults": { "source": "s3", "s3": { /* …shared bucket/creds… */ "bucket": "my-uploads" } },
"inputs": [
{ "path": "raw/a.mov", "out_subpath": "a/" },
{ "path": "raw/b.mov", "out_subpath": "b/" },
{ "url": "https://example.com/c.mp4", "out_subpath": "c/" }
]
}
Each inputs[] item is a full s3 block, a url, or just a path that inherits input_defaults.s3. The response returns a batch_id and the list of job_ids; list a batch later with GET /transcoder/jobs?batch_id=<id>.
Avoid duplicate jobs (idempotency)
Pass an "idempotency_key": "your-unique-key" on POST /transcoder/jobs. If a job with that key already exists for your organization, we return the existing job instead of creating a second one, so it's safe to retry a request that may have already gone through.
Cancel a job
curl -X DELETE https://api.cubepath.com/transcoder/jobs/<uuid> \
-H "Authorization: Bearer $CUBEPATH_TOKEN" \
-H "X-Requested-With: XMLHttpRequest"
This stops any not-yet-started work and sets status: "canceled". A job that's already completed or failed returns 409 Conflict.
Limits
| Limit | Value |
|---|---|
| Outputs per job | 20 |
| Inputs per batch request | 1000 |
| Active (unfinished) jobs per organization | 20,000 |
| Create-job rate | 60 requests / minute |
| Batch rate | 10 requests / minute |
What the Transcoding API doesn't do
- It doesn't store your media. Input and output both live in your buckets. There's no "download from CubePath" step; finished files are already in your destination.
- It isn't a streaming server. It produces HLS you can stream; serving it (and access control, signed URLs, etc.) is your CDN/origin's job.
- It isn't an editor. It transcodes, packages, thumbnails, and clips to GIF; it doesn't cut, splice, overlay, or color-grade.
Troubleshooting
| Symptom | Likely cause |
|---|---|
400 on create | Missing field for the source type: input.url is required when source is url, input.s3 when source is s3 |
400 "cannot target a private/internal address" | A URL or S3 endpoint points at a private/internal IP; use a public hostname |
401 / 403 | Missing/expired token, or the token lacks transcoder:read / transcoder:write |
403 on write only | Missing the X-Requested-With: XMLHttpRequest header, or the organization is unverified/suspended/out of balance |
429 | You hit the create/batch rate limit or the active-jobs cap; wait for jobs to finish |
| HLS output is empty | The hls output needs a ladder array of rungs |
status: failed | Read the error field; usually an unreadable source, unreachable S3 endpoint, or wrong output credentials |
Job stuck queued | Normal under load; encoding starts as capacity frees up, so watch progress |
API reference
POST /transcoder/jobs Create one job. (scope: transcoder:write)
POST /transcoder/jobs/batch Create up to 1000 jobs in one call. (scope: transcoder:write)
GET /transcoder/jobs List your jobs. ?limit ?offset ?batch_id (scope: transcoder:read)
GET /transcoder/jobs/{uuid} Job status & progress. (scope: transcoder:read)
GET /transcoder/jobs/{uuid}/outputs Produced file locations. (scope: transcoder:read)
DELETE /transcoder/jobs/{uuid} Cancel a job. (scope: transcoder:write)
All requests are authenticated with Authorization: Bearer <token> (or X-API-Key). Write requests also need X-Requested-With: XMLHttpRequest.
