API de Transcodificación: Convierte vídeo a escala en tu propio bucket

La API de Transcodificación convierte un vídeo de origen en las versiones que realmente publicas (archivos MP4 en varias resoluciones, una escalera adaptativa HLS, sprites de miniaturas, GIFs de previsualización) y los escribe directamente en tu propio bucket compatible con S3. Tú nos das una entrada y un destino; nosotros leemos, codificamos y entregamos. Nunca guardamos tu contenido.

Es el patrón habitual para plataformas VOD, pipelines de contenido generado por usuarios, procesado de cursos/webinars, previsualizaciones para redes sociales, y cualquier caso en el que tengas una pila de archivos de origen y necesites una salida multiformato y consistente sin ejecutar ffmpeg tú mismo.

Cómo funciona (en un párrafo)

Creas un trabajo (job): una entrada (una URL pública o un objeto de tu bucket S3), un destino de salida (tu bucket S3 + un prefijo de ruta) y una lista de outputs, los formatos que quieres. Obtenemos el origen, codificamos cada versión en paralelo, empaquetamos los resultados y los subimos bajo tu prefijo de destino. El trabajo pasa por queued → analyzing → encoding → finalizing → completed, exponiendo un progreso de 0–100 durante todo el proceso. Al terminar podemos avisar a un webhook que indiques, y los archivos generados quedan en tu bucket. Tus credenciales de almacenamiento se usan solo para ese trabajo y se guardan cifradas; nunca las devolvemos ni conservamos la salida.

Antes de empezar

  • Consigue un token de API en el panel: my.cubepath.com → Cuenta → Tokens de API. Envíalo en cada petición como Authorization: Bearer <token> (o X-API-Key: <token>).
  • Scopes: consultar el estado necesita transcoder:read; crear o cancelar trabajos necesita transcoder:write.
  • Las peticiones de escritura (POST, DELETE) deben llevar también la cabecera X-Requested-With: XMLHttpRequest.
  • Estado de la cuenta: tu organización debe estar verificada, sin suspensión y con saldo positivo para crear trabajos.
  • URL base: https://api.cubepath.com.

El almacenamiento lo pones tú. Tanto la entrada (cuando es S3) como la salida apuntan a cualquier endpoint compatible con S3: CubePath Object Storage, AWS S3, Cloudflare R2, Backblaze B2, Wasabi, MinIO. El secret_key que envías se cifra en reposo y nunca se devuelve en ninguna respuesta de la API.

Crea tu primer trabajo

Un trabajo mínimo: coger un MP4 y producir un MP4 a 1080p y otro a 720p en tu 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/charla.mp4"
    },
    "output": {
      "s3": {
        "endpoint": "https://s3.eu-central-1.amazonaws.com",
        "region": "eu-central-1",
        "bucket": "mi-media",
        "path": "charlas/charla-42/",
        "access_key": "AKIA...",
        "secret_key": "..."
      }
    },
    "outputs": [
      { "type": "file", "container": "mp4", "codec": "h264", "height": 1080 },
      { "type": "file", "container": "mp4", "codec": "h264", "height": 720 }
    ]
  }'

La respuesta es el trabajo creado, con su uuid y status: "queued". Guarda el uuid; es como consultas el progreso y obtienes las salidas.

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/charla.mp4"},
    "output": {"s3": {
        "endpoint": "https://s3.eu-central-1.amazonaws.com",
        "region": "eu-central-1", "bucket": "mi-media", "path": "charlas/charla-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"])

Leer la entrada desde tu bucket en lugar de una URL

Pon input.source en "s3" e indica la ubicación del objeto:

"input": {
  "source": "s3",
  "s3": {
    "endpoint": "https://s3.eu-central-1.amazonaws.com",
    "region": "eu-central-1",
    "bucket": "mis-subidas",
    "path": "entrantes/charla.mp4",      // la clave del objeto
    "access_key": "AKIA...",
    "secret_key": "..."
  }
}

Tipos de salida

Cada entrada de outputs (de 1 a 20 por trabajo) es un formato. Combínalos libremente en un mismo trabajo; todos leen el origen una sola vez.

typeParámetros claveProduce
filecodec (h264/h265/vp9/av1), container (mp4/webm/…, por defecto mp4), height (0 = mantener origen), opcional crf, preset, bitrateoutput_<i>.<container> (p. ej. output_0.mp4)
hlsladder: un array de niveles [{ "height": 1080 }, { "height": 720 }, …] (obligatorio; sin ladder no se produce nada)hls<i>/master.m3u8 + listas de reproducción y segmentos por nivel
thumbnailsinterval (segundos entre fotogramas)thumbs<i>/thumb_0001.jpg, thumb_0002.jpg, …
gifduration (segundos), opcional start ("00:00:05")output_<i>.gif

Un trabajo que produce una escalera adaptativa más miniaturas y un GIF de previsualización:

"outputs": [
  { "type": "hls", "ladder": [ { "height": 1080 }, { "height": 720 }, { "height": 480 } ] },
  { "type": "thumbnails", "interval": 10 },
  { "type": "gif", "start": "00:00:05", "duration": 3 }
]

Los nombres de campo importan. file lee codec (no video_codec). hls necesita un ladder. codec acepta h264, h265, vp9, av1. Cualquier altura funciona, incluidas 2160 (4K) y 4320 (8K). Recuerda que 4K/8K y los códecs más lentos (h265, av1) consumen mucho más tiempo de procesamiento.

Consultar estado y progreso

curl https://api.cubepath.com/transcoder/jobs/<uuid> \
  -H "Authorization: Bearer $CUBEPATH_TOKEN"

Campos clave de la respuesta:

CampoSignificado
statusqueuedanalyzingencodingfinalizingcompleted (o failed / canceled)
progress0100
total_segments / completed_segmentsCuánto de la codificación va hecho (los archivos grandes se dividen y codifican en paralelo)
outputsSe rellena al completarse el trabajo, con los archivos generados
errorEl motivo, si status es failed

Consulta este endpoint por sondeo, o define un webhook_url (más abajo) para no tener que hacerlo.

Obtener las salidas

Cuando el trabajo está completed, obtén las ubicaciones de los artefactos generados:

curl https://api.cubepath.com/transcoder/jobs/<uuid>/outputs \
  -H "Authorization: Bearer $CUBEPATH_TOKEN"

Todo se escribe bajo el prefijo path que fijaste en output.s3. Para un prefijo de salida charlas/charla-42/:

charlas/charla-42/output_0.mp4
charlas/charla-42/output_1.mp4
charlas/charla-42/hls0/master.m3u8
charlas/charla-42/thumbs1/thumb_0001.jpg
charlas/charla-42/output_2.gif

Los archivos viven en tu bucket; sírvelos directamente, o pon una zona CDN delante. Cualquier archivo temporal que usemos durante el proceso se limpia automáticamente tras un final correcto.

Webhooks

Añade "webhook_url": "https://example.com/hooks/transcode" a un trabajo y haremos un POST con JSON al terminar:

// éxito
{ "job_id": "…", "status": "completed" }
// fallo
{ "job_id": "…", "status": "failed", "error": "<motivo>" }

La entrega es de mejor esfuerzo y no se reintenta, así que trata el webhook como un aviso y mantén GET /transcoder/jobs/{uuid} como la fuente de verdad.

Procesar muchos archivos a la vez

POST /transcoder/jobs/batch crea hasta 1000 trabajos en una sola llamada. Comparten un mismo destino output y una misma especificación outputs; cada entrada añade un out_subpath opcional que se concatena al prefijo de salida para que los trabajos no se sobrescriban entre sí.

{
  "output": { "s3": { /* …bucket de destino… */ "path": "biblioteca/" } },
  "outputs": [ { "type": "file", "container": "mp4", "codec": "h264", "height": 720 } ],
  "input_defaults": { "source": "s3", "s3": { /* …bucket/credenciales compartidas… */ "bucket": "mis-subidas" } },
  "inputs": [
    { "path": "raw/a.mov", "out_subpath": "a/" },
    { "path": "raw/b.mov", "out_subpath": "b/" },
    { "url": "https://example.com/c.mp4", "out_subpath": "c/" }
  ]
}

Cada elemento de inputs[] es un bloque s3 completo, una url, o solo un path que hereda input_defaults.s3. La respuesta devuelve un batch_id y la lista de job_ids; lista un lote más tarde con GET /transcoder/jobs?batch_id=<id>.

Evitar trabajos duplicados (idempotencia)

Pasa una "idempotency_key": "tu-clave-unica" en POST /transcoder/jobs. Si ya existe un trabajo con esa clave en tu organización, devolvemos el trabajo existente en lugar de crear uno segundo, así es seguro reintentar una petición que quizá ya pasó.

Cancelar un trabajo

curl -X DELETE https://api.cubepath.com/transcoder/jobs/<uuid> \
  -H "Authorization: Bearer $CUBEPATH_TOKEN" \
  -H "X-Requested-With: XMLHttpRequest"

Esto detiene el trabajo aún no iniciado y fija status: "canceled". Un trabajo que ya esté completed o failed devuelve 409 Conflict.

Límites

LímiteValor
Salidas por trabajo20
Entradas por petición de lote1000
Trabajos activos (sin terminar) por organización20.000
Ritmo de creación de trabajos60 peticiones / minuto
Ritmo de lotes10 peticiones / minuto

Lo que la API de Transcodificación no hace

  • No almacena tu contenido. La entrada y la salida viven en tus buckets. No hay un paso de "descargar desde CubePath"; los archivos terminados ya están en tu destino.
  • No es un servidor de streaming. Produce HLS que puedes emitir; servirlo (y el control de acceso, URLs firmadas, etc.) es tarea de tu CDN/origen.
  • No es un editor. Transcodifica, empaqueta, genera miniaturas y recorta a GIF; no corta, une, superpone ni corrige color.

Solución de problemas

SíntomaCausa probable
400 al crearFalta un campo según el tipo de origen: input.url es obligatorio cuando source es url, input.s3 cuando source es s3
400 "cannot target a private/internal address"Una URL o endpoint S3 apunta a una IP privada/interna; usa un nombre de host público
401 / 403Token ausente/caducado, o el token no tiene transcoder:read / transcoder:write
403 solo en escrituraFalta la cabecera X-Requested-With: XMLHttpRequest, o la organización está sin verificar/suspendida/sin saldo
429Has alcanzado el límite de ritmo de creación/lote o el tope de trabajos activos; espera a que terminen
Salida HLS vacíaLa salida hls necesita un array ladder con niveles
status: failedLee el campo error; normalmente un origen ilegible, un endpoint S3 inalcanzable o credenciales de salida incorrectas
Trabajo atascado en queuedNormal con carga alta; la codificación arranca según se libera capacidad, así que observa progress

Referencia de la API

POST   /transcoder/jobs                  Crear un trabajo.                        (scope: transcoder:write)
POST   /transcoder/jobs/batch            Crear hasta 1000 trabajos en una llamada. (scope: transcoder:write)
GET    /transcoder/jobs                  Listar tus trabajos. ?limit ?offset ?batch_id (scope: transcoder:read)
GET    /transcoder/jobs/{uuid}           Estado y progreso del trabajo.           (scope: transcoder:read)
GET    /transcoder/jobs/{uuid}/outputs   Ubicación de los archivos producidos.    (scope: transcoder:read)
DELETE /transcoder/jobs/{uuid}           Cancelar un trabajo.                     (scope: transcoder:write)

Todas las peticiones se autentican con Authorization: Bearer <token> (o X-API-Key). Las peticiones de escritura necesitan además X-Requested-With: XMLHttpRequest.