API de Cinestoria
Crea vídeos desde un tema o un guion, sigue su producción, aprueba el guion y descarga el resultado desde tu propio sistema, Zapier, Make o n8n. Todo en JSON sobre HTTPS, en https://cinestoria.com/api/v1. Está en los planes Canal y Estudio.
Los vídeos se producen con las claves de proveedores que tengas conectadas en Cinestoria y respetan tu plan, tu tope de gasto y tus canales, igual que en la web.
Autenticación
Crea una clave en API y webhooks y mándala en cada petición. Se enseña una sola vez: guárdala como un secreto. Si se filtra, anúlala y crea otra.
Authorization: Bearer cin_…
Comprueba que funciona:
curl https://cinestoria.com/api/v1/me -H "Authorization: Bearer cin_…"
Límites
- 120 peticiones por minuto con cada clave.
- 30 vídeos nuevos por hora en cada cuenta.
- 20 lotes por hora en cada cuenta (cada uno, hasta 30 vídeos).
- Al pasarte, la respuesta es
429con la cabeceraRetry-After(segundos).
Crear un vídeo
POST/api/v1/videos
Manda topic (Cinestoria investiga y escribe el guion) o script (tu guion), no los dos. Responde 201 con el vídeo. Con la cabecera Idempotency-Key, si repites la misma petición (por ejemplo, un reintento de Zapier) te devuelve el vídeo que ya creó en lugar de hacer otro.
curl -X POST https://cinestoria.com/api/v1/videos \
-H "Authorization: Bearer cin_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1234" \
-d '{"channel_id": 1, "topic": "El faro que nadie podía construir",
"minutes": 6, "quality": "estandar", "review": false}'
| Campo | Qué es |
|---|---|
| channel_id | Obligatorio. Uno de tus canales (GET /api/v1/channels). El vídeo usa su voz, su estilo y su memoria. |
| topic | El tema (hasta 1.000 caracteres). |
| script | Tu guion completo (al menos un par de párrafos). |
| format | 16:9, 9:16, short. Por defecto, el del canal. short es un Short vertical de menos de un minuto. |
| minutes | Duración aproximada (1–30) cuando mandas un tema. |
| quality | economico, estandar, premium. Por defecto, la del canal. |
| review | true (por defecto): se para cuando el guion está listo para que lo revises (evento video.review) antes de gastar en voz e imágenes. false: sigue hasta el final. |
| storyboard | Bocetos baratos para revisar antes de las imágenes buenas. Por defecto, lo que tenga el canal. |
| subtitles | Subtítulos en la imagen (true por defecto en vertical y Shorts; false en horizontal). El .srt se entrega siempre. |
| sound | Sonido ambiente (true por defecto). |
| music | ai, tension, calma, epica, none. Vacío: la del canal. |
| language | es, en, pt, fr, de, it. Vacío: el del canal. |
| genre | documental, empresas, sabiduria, espiritual, terror, ilustrada, relato. |
| narration | voz (con narrador) o texto (sin narrador: la historia se lee en pantalla, pensado para relatos). Vacío: la del canal. |
| person | Relatos: primera (lo cuenta el protagonista) o segunda («tú» lo vives). Vacío: la del canal. |
| graphics, archive, clips | Grafismo (ninguno, ligero, normal, intenso), imágenes reales de archivo (ninguno, algo, mucho) y clips de vídeo (none, hook, chapters, medio, maximo, ratio, key, all). |
Crear varios vídeos de una vez
POST/api/v1/videos/batch
Los mismos campos que un vídeo, pero con topics: una lista de temas (hasta 30). Se crea un vídeo por tema, todos con los mismos ajustes. Antes de crear ninguno se comprueba el lote entero (tu plan, tu tope de gasto para todos): o se crean todos o ninguno. Responde 201 con data, la lista de vídeos. Con Idempotency-Key, repetir la petición devuelve el mismo lote.
Con "skip_similar": true no se crean los temas casi iguales a un vídeo que el canal ya tiene (o a otro tema del lote): la respuesta los lista en skipped. Si se saltan todos, 400 con el código all_similar.
curl -X POST https://cinestoria.com/api/v1/videos/batch \
-H "Authorization: Bearer cin_…" \
-H "Content-Type: application/json" \
-d '{"channel_id": 1, "minutes": 8, "quality": "estandar",
"topics": ["El faro que nadie podía construir", "El error de unidades que hizo perder una sonda en Marte"]}'
Todas las rutas
GET/api/v1/me
Tu cuenta, tu plan y el nombre de la clave.
GET/api/v1/channels
Tus canales con su idioma, formato, calidad y duración por defecto.
GET/api/v1/videos
Tus vídeos, del más nuevo al más antiguo. Filtros: state, channel_id, limit (hasta 100) y before_id para pasar de página (usa next_before_id de la respuesta).
POST/api/v1/videos/batch
Varios vídeos de una vez, uno por tema (cómo).
GET/api/v1/videos/{id}
Un vídeo, con su progreso y, si se ha subido, sus subidas a YouTube.
GET/api/v1/videos/{id}/script
El guion, escena a escena (para revisarlo en tu sistema).
POST/api/v1/videos/{id}/approve
Aprueba el guion o el storyboard tal cual, como el botón de la web. Queda apuntado que lo aprobó la API con esa clave. Si el vídeo no espera revisión: 409.
POST/api/v1/videos/{id}/derivados
Escribe, a partir del guion de un vídeo terminado, un artículo para el blog, un hilo para X, un post de LinkedIn, un texto de Instagram, una newsletter y una publicación de comunidad (unos céntimos en tu cuenta de Anthropic). Responde 202 y tarda alrededor de un minuto.
GET/api/v1/videos/{id}/derivados
Su estado (none, running, done o error) y, cuando están, las piezas en pieces: articulo (titulo, meta, markdown), hilo, linkedin, instagram, newsletter y comunidad.
GET/api/v1/videos/{id}/files/{nombre}
Descarga un archivo de un vídeo terminado: video.mp4, miniatura.jpg, subtitulos.srt, descripcion.txt, short_1.mp4… Las rutas completas vienen en files y shorts del vídeo. Los archivos se borran por antigüedad (expired).
El objeto vídeo
{
"id": 42, "kind": "video", "parent_id": null, "channel_id": 1,
"state": "done", "state_label": "Terminado",
"title": "El faro que nadie podía construir", "topic": "El faro…", "format": "16:9",
"created": "2026-10-28T09:12:03Z", "finished": "2026-10-28T09:41:55Z",
"progress": 100, "stage": null, "error": null, "cost_eur": 3.12, "expired": false,
"files": {"video": "https://cinestoria.com/api/v1/videos/42/files/video.mp4",
"thumbnail": "https://cinestoria.com/api/v1/videos/42/files/miniatura.jpg",
"subtitles": "https://cinestoria.com/api/v1/videos/42/files/subtitulos.srt"},
"shorts": [],
"metadata": {"title": "…", "description": "…", "tags": ["faros", "ingeniería"],
"chapters": [{"start": 0.0, "title": "La roca"}, {"start": 74.5, "title": "El faro"}],
"duration": 362.4},
"review": {"score": 9.5, "warnings": ["sonoridad"], "frames": []},
"app_url": "https://cinestoria.com/app/videos/42"
}
review es la revisión final del vídeo terminado: la nota sobre 10, las comprobaciones que avisan y los fotogramas con algún problema (segundo y tipo). Es null hasta que está hecha.
| state | Qué significa |
|---|---|
| queued | En cola |
| running | En producción |
| review | Esperando la revisión del guion |
| storyboard | Esperando la revisión del storyboard |
| done | Terminado |
| error | Error |
| stopped | Detenido |
Webhooks
En lugar de preguntar cada poco, añade en API y webhooks la URL que debe recibir los avisos. Cinestoria le hace un POST con JSON cuando pasa algo:
| Evento | Cuándo |
|---|---|
| video.review | Guion listo para revisar |
| video.storyboard | Storyboard listo para revisar |
| video.done | Vídeo terminado |
| video.error | Error en un vídeo |
| youtube.uploaded | Subido a YouTube (publicado, programado o privado) |
| ping | Al pulsar «Probar» |
POST https://tu-servidor.com/cinestoria
Content-Type: application/json
Cinestoria-Event: video.done
Cinestoria-Delivery: evt_1234
Cinestoria-Signature: t=1793100000,v1=5f1c…
{"id": "evt_1234", "type": "video.done", "created": 1793100000,
"data": { …el objeto vídeo… }}
- Responde con un código
2xxen menos de 10 segundos. Si no, se reintenta al cabo de 1 min, 5 min, 30 min, 2 h y 6 h. - Tras 20 fallos seguidos el webhook se pausa y te avisamos por email. Lo reanudas con un botón.
- Un mismo aviso puede llegar dos veces: usa
idpara no procesarlo dos veces. - Solo URLs
httpsa direcciones públicas, sin redirecciones.
Comprobar la firma
Cada webhook tiene su secreto (whsec_…), que ves una vez al crearlo. La cabecera Cinestoria-Signature lleva t (segundos) y v1: el HMAC-SHA256, en hexadecimal, de "{t}.{cuerpo}" con ese secreto. Calcúlalo con el cuerpo tal cual llega (sin volver a formatear el JSON), compáralo en tiempo constante y rechaza los avisos de hace más de 5 minutos.
# Python
import hashlib, hmac, time
def valida(cuerpo: bytes, cabecera: str, secreto: str) -> bool:
partes = dict(p.split("=", 1) for p in cabecera.split(","))
t, firma = partes["t"], partes["v1"]
esperada = hmac.new(secreto.encode(), f"{t}.".encode() + cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, firma) and abs(time.time() - int(t)) < 300
// Node.js
const crypto = require("crypto");
function valida(cuerpo, cabecera, secreto) {
const p = Object.fromEntries(cabecera.split(",").map(x => x.split("=")));
const esperada = crypto.createHmac("sha256", secreto).update(`${p.t}.${cuerpo}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(p.v1))
&& Math.abs(Date.now() / 1000 - Number(p.t)) < 300;
}
Errores
Los errores llegan con su código HTTP y un JSON con un code estable y un message para personas:
{"error": {"code": "invalid_channel", "message": "channel_id tiene que ser uno de tus canales…"}}
| HTTP | code | Qué pasa |
|---|---|---|
| 401 | missing_key, invalid_key | Falta la clave, no existe o está anulada. |
| 402 | blocked | Tu cuenta no puede producir ahora (pago pendiente, email sin confirmar…). |
| 403 | plan | Tu plan no incluye la API. |
| 404 | not_found, no_script | Ese vídeo o archivo no existe en tu cuenta. |
| 400 | invalid_json, invalid_request, invalid_channel, invalid_state, rejected | La petición no vale; message dice por qué (por ejemplo, el tope de gasto). |
| 409 | missing_keys, not_waiting | Faltan claves de proveedores en Conexiones, o el vídeo no espera revisión. |
| 429 | rate_limited | Demasiadas peticiones: espera lo que diga Retry-After. |