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> (or X-API-Key: <token>).
  • Scopes: reading job status needs transcoder:read; creating or canceling jobs needs transcoder:write.
  • Write requests (POST, DELETE) must also carry the header X-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_key you 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.

typeKey parametersProduces
filecodec (h264/h265/vp9/av1), container (mp4/webm/…, default mp4), height (0 = keep source), optional crf, preset, bitrateoutput_<i>.<container> (e.g. output_0.mp4)
hlsladder: an array of rungs [{ "height": 1080 }, { "height": 720 }, …] (required; no ladder means nothing is produced)hls<i>/master.m3u8 + per-rung playlists and segments
thumbnailsinterval (seconds between frames)thumbs<i>/thumb_0001.jpg, thumb_0002.jpg, …
gifduration (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. file reads codec (not video_codec). hls needs a ladder. codec accepts h264, h265, vp9, av1. Any height works, including 2160 (4K) and 4320 (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:

FieldMeaning
statusqueuedanalyzingencodingfinalizingcompleted (or failed / canceled)
progress0100
total_segments / completed_segmentsHow much of the encode is done (big files are split and encoded in parallel)
outputsPopulated when the job completes, with the produced files
errorThe 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

LimitValue
Outputs per job20
Inputs per batch request1000
Active (unfinished) jobs per organization20,000
Create-job rate60 requests / minute
Batch rate10 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

SymptomLikely cause
400 on createMissing 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 / 403Missing/expired token, or the token lacks transcoder:read / transcoder:write
403 on write onlyMissing the X-Requested-With: XMLHttpRequest header, or the organization is unverified/suspended/out of balance
429You hit the create/batch rate limit or the active-jobs cap; wait for jobs to finish
HLS output is emptyThe hls output needs a ladder array of rungs
status: failedRead the error field; usually an unreadable source, unreachable S3 endpoint, or wrong output credentials
Job stuck queuedNormal 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.