Cut.ProDocs
Guías

Recetas

Los tres flujos que resuelven casi todo, listos para copiar y ejecutar

El Quickstart muestra cada petición por separado. Aquí están los flujos enteros, tal como corren en producción. Copia, cambia la URL y el connection_id, ejecuta.

1. Del enlace de YouTube a la publicación

El camino completo, sin nadie en el medio: analiza, recorta, elige el mejor corte, aplica tu plantilla, renderiza y publica en TikTok e Instagram.

Los dos pasos que cuestan: POST /clips gasta créditos, y publicar consume la cuota diaria de la cuenta conectada. El resto es gratis.

const API = "https://api.cut.pro/api/v1";
const HEADERS = { "X-Api-Key": process.env.CUTPRO_API_KEY, "Content-Type": "application/json" };

async function call(path, init) {
	const response = await fetch(`${API}${path}`, { ...init, headers: HEADERS });
	const body = await response.json();
	if (!response.ok) throw new Error(`${response.status} ${body.code}`);
	return body;
}

const wait = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

/** Repite `fn` hasta que el estado salga de la lista de "aún trabajando". */
async function until(fn, working, everySeconds) {
	let state = await fn();
	while (working.includes(state.status)) {
		await wait(everySeconds);
		state = await fn();
	}
	return state;
}

const SOURCE = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
const TEMPLATE_ID = "7399000000000001";

// Cada destino lleva la metadata de SU red: las claves de dentro son de la
// plataforma, no nuestras. Lista completa en /es/api-reference/postagem.
const targetsFor = (clip) => [
	{ connection_id: "7399001244553012", metadata: { tiktok: { title: clip.title, privacyLevel: "PUBLIC_TO_EVERYONE" } } },
	{ connection_id: "7399001244553099", metadata: { instagram: { caption: clip.title } } },
];

// 1. Cuánto cuesta, antes de gastar.
const video = await call("/clips/info", { method: "POST", body: JSON.stringify({ url: SOURCE }) });
console.log(`${video.title}: ${video.credits_cost} créditos`);

// 2. Recortar. Aquí salen los créditos.
const submission = await call("/clips", {
	method: "POST",
	body: JSON.stringify({ video_id: video.id, template_id: TEMPLATE_ID }),
});

const base = `/clips/${video.id}/submissions/${submission.id}`;
const done = await until(() => call(base), ["queued", "downloading", "transcribing", "video_analysis", "analyzing", "finalizing"], 15);
if (done.status === "failed") throw new Error(done.error_code);

// 3. El mejor corte según la nota de la IA.
const { clips } = await call(`${base}/clips?sort=rating&order=desc&limit=1`);
const best = clips[0];

// 4. Renderizar. `from_cache` significa que el MP4 ya existía.
const render = await call(`${base}/clips/${best.id}/render`, { method: "POST" });
const finished = render.from_cache ? render : await until(() => call(`/renders/${render.id}`), ["queued", "active"], 10);
if (finished.status !== "completed") throw new Error(finished.status);

// 5. Publicar el MISMO edit en las dos cuentas.
const post = await call("/posts", {
	method: "POST",
	body: JSON.stringify({ videos: [{ edit_id: render.edit_id, targets: targetsFor(best) }] }),
});

// 6. Seguirlo hasta que cada cuenta responda.
const published = await until(() => call(`/posts/${post.id}`), ["pending", "processing"], 10);
for (const item of published.items) {
	console.log(item.platform, item.status, item.published_url ?? item.error_code);
}

Una publicación puede terminar en partial: una cuenta publicó, otra falló. No lo trates como éxito. Recorre los items y usa POST /posts/{id}/items/{itemId}/retry en los que fallaron, sin tocar los que ya están publicados.

2. Una cola de varios vídeos

Recortar un canal entero es el mismo flujo, N veces. Lo que cambia es que no dispares todo de golpe: cada envío en marcha ocupa la cola, y el polling de todos a la vez es lo que se acerca al límite de peticiones.

const SOURCES = ["https://youtu.be/aaa", "https://youtu.be/bbb", "https://youtu.be/ccc"];
const AT_A_TIME = 3;

async function clipOne(url) {
	const video = await call("/clips/info", { method: "POST", body: JSON.stringify({ url }) });
	const submission = await call("/clips", { method: "POST", body: JSON.stringify({ video_id: video.id }) });
	const base = `/clips/${video.id}/submissions/${submission.id}`;
	const done = await until(() => call(base), ["queued", "downloading", "transcribing", "video_analysis", "analyzing", "finalizing"], 15);
	if (done.status === "failed") return { url, error: done.error_code };
	const { clips } = await call(`${base}/clips`);
	return { url, clips };
}

// Una cinta con N trabajadores tirando de la misma lista: nunca más de N a la vez.
const queue = [...SOURCES];
const results = await Promise.all(
	Array.from({ length: AT_A_TIME }, async () => {
		const mine = [];
		while (queue.length > 0) mine.push(await clipOne(queue.shift()));
		return mine;
	}),
);

console.log(results.flat());

Antes de lanzar el lote, GET /balance dice si el saldo lo cubre, y GET /renders/limits dice cuántos renders simultáneos acepta tu plan. Pasarte del segundo devuelve 429 en el render, no en el recorte.

Cuando el envío se queda en downloading

No está colgado. Si la fuente todavía no existe por completo, el campo waiting_reason lo explica y el trabajo se reanuda solo:

waiting_reasonQué está pasando
SOURCE_PREMIERE_SCHEDULEDUn estreno programado que aún no ha empezado. waiting_until trae la hora anunciada
SOURCE_LIVE_IN_PROGRESSLa transmisión sigue en directo
SOURCE_VOD_PROCESSINGTerminó el directo, la plataforma aún está publicando la grabación

Muestra eso a tu usuario en vez de un "procesando" genérico. Si no quiere esperar, DELETE /clips/{videoId}/submissions/{submissionId} devuelve los créditos mientras waiting_reason esté puesto.

3. Un MP4 nuevo de un clip antiguo

El archivo renderizado caduca según tu plan (GET /renders/limits trae el render_expiry_hours). El edit no caduca nunca. Guarda el edit_id y generas un archivo nuevo cuando quieras, sin volver a recortar ni a pagar.

curl -X POST https://api.cut.pro/api/v1/renders \
  -H "X-Api-Key: $CUTPRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "edit_id": "7412908490112775" }'
Respuesta 202
{
  "id": "7412908490998001",
  "edit_id": "7412908490112775",
  "status": "queued",
  "output_resolution": "1080p",
  "has_watermark": false,
  "from_cache": false,
  "download_url": null
}

Si el archivo idéntico sigue en el almacenamiento, la respuesta llega con 200, from_cache: true y el download_url ya puesto: no se renderizó nada de nuevo.

Por eso vale la pena guardar el edit_id de cada publicación en tu base de datos. Un elemento cuyo render_id pasó a null perdió el archivo, no el edit: POST /renders con su edit_id trae el vídeo de vuelta.

En esta página