Cut.ProDocs
Guias

Receitas

Os três fluxos que resolvem quase tudo, prontos para copiar e rodar

O Quickstart mostra cada requisição isolada. Aqui estão os fluxos inteiros, do jeito que rodam em produção. Copie, troque a URL e o connection_id, rode.

1. Do link do YouTube ao post publicado

O caminho completo, sem intervenção humana: analisa, clipa, escolhe o melhor corte, aplica o seu template, renderiza e publica no TikTok e no Instagram.

Os dois passos que custam: POST /clips debita créditos, e publicar consome a cota diária da conta conectada. O resto é de graça.

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));

/** Repete `fn` até o estado sair da lista de "ainda trabalhando". */
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 traz a metadata da SUA rede: as chaves de dentro são as da
// plataforma, não as nossas. Veja a lista completa em /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. Quanto custa, 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. Clipar. Aqui os créditos saem.
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. O melhor corte pela nota da IA.
const { clips } = await call(`${base}/clips?sort=rating&order=desc&limit=1`);
const best = clips[0];

// 4. Renderizar. `from_cache` significa que o MP4 já existia.
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 o MESMO edit nas duas contas.
const post = await call("/posts", {
	method: "POST",
	body: JSON.stringify({ videos: [{ edit_id: render.edit_id, targets: targetsFor(best) }] }),
});

// 6. Acompanhar até cada conta responder.
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);
}

Um post pode terminar em partial: uma conta publicou, outra falhou. Não trate isso como sucesso. Olhe item por item e use POST /posts/{id}/items/{itemId}/retry no que falhou, sem mexer no que já subiu.

2. Uma fila de vários vídeos

Clipar um canal inteiro é o mesmo fluxo, N vezes. O que muda é você não disparar tudo de uma vez: cada submissão em voo ocupa a fila, e o polling de todas juntas é o que encosta no limite de requisições.

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 };
}

// Uma esteira com N trabalhadores puxando da mesma lista: nunca mais que N em voo.
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 disparar o lote, GET /balance diz se o saldo cobre tudo, e GET /renders/limits diz quantos renders simultâneos o seu plano aceita. Estourar o segundo devolve 429 no render, não na clipagem.

Quando a submissão fica parada em downloading

Não é travamento. Se a fonte ainda não existe por inteiro, o campo waiting_reason explica e o job retoma sozinho:

waiting_reasonO que está acontecendo
SOURCE_PREMIERE_SCHEDULEDEstreia agendada que ainda não começou. waiting_until traz a hora anunciada
SOURCE_LIVE_IN_PROGRESSA transmissão ainda está no ar
SOURCE_VOD_PROCESSINGAcabou a live, a plataforma ainda está publicando a gravação

Mostre isso para o seu usuário em vez de um "processando" genérico. Se ele não quiser esperar, DELETE /clips/{videoId}/submissions/{submissionId} devolve os créditos enquanto waiting_reason estiver preenchido.

3. Um MP4 novo de um clipe antigo

O arquivo renderizado expira segundo o seu plano (GET /renders/limits traz o render_expiry_hours). O edit não expira nunca. Guarde o edit_id e você gera um arquivo novo quando quiser, sem clipar nem pagar de novo.

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" }'
Resposta 202
{
  "id": "7412908490998001",
  "edit_id": "7412908490112775",
  "status": "queued",
  "output_resolution": "1080p",
  "has_watermark": false,
  "from_cache": false,
  "download_url": null
}

Se o arquivo idêntico ainda estiver no armazenamento, a resposta vem 200 com from_cache: true e o download_url já preenchido: nada foi renderizado de novo.

É por isso que vale guardar o edit_id de cada post no seu banco. Um item de post cujo render_id virou null perdeu o arquivo, não o edit: POST /renders com o edit_id dele traz o vídeo de volta.

Nesta página