Cut.ProDocs

Recipes

Whole flows ready to copy: from link to published post, a queue of videos and the best moments of a live stream posted automatically.

Each page of the guide shows one stage. Here the stages come together, the way they run in production. Swap the URLs, the template_id and the connection_id values and run with CUTPRO_API_KEY in the environment.

From the YouTube link to the published post

Analyzes, clips with your template, picks the highest-scoring clip, renders and publishes to TikTok and Instagram.

POST /clips charges credits and the post uses each account's daily quota. The rest is free.

const API = "https://api.cut.pro/api/v1";
const HEADERS = { "X-Api-Key": process.env.CUTPRO_API_KEY, "Content-Type": "application/json" };
const CLIPPING = ["queued", "downloading", "transcribing", "video_analysis", "analyzing", "finalizing"];

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

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

async function until(read, working, everySeconds) {
	let state = await read();
	while (working.includes(state.status)) {
		await new Promise((resolve) => setTimeout(resolve, everySeconds * 1000));
		state = await read();
	}
	return state;
}

const video = await call("/clips/info", { method: "POST", body: JSON.stringify({ url: SOURCE }) });
console.log(`${video.title}: ${video.credits_cost} credits`);

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), CLIPPING, 15);
if (done.status === "failed") throw new Error(done.error_code);

const { clips } = await call(`${base}/clips?sort=rating&order=desc&limit=1`);
const best = clips[0];

const render = await call(`${base}/clips/${best.id}/render`, { method: "POST" });
const finished = render.from_cache ? render : await until(() => call(`/renders/${render.id}`), ["waiting_quality", "queued", "active"], 10);
if (finished.status !== "completed") throw new Error(finished.status);

const post = await call("/posts", {
	method: "POST",
	body: JSON.stringify({
		videos: [
			{
				edit_id: render.edit_id,
				targets: [
					{ connection_id: "7390012299881100", metadata: { tiktok: { title: best.title, privacyLevel: "PUBLIC_TO_EVERYONE" } } },
					{ connection_id: "7390012299881300", metadata: { instagram: { caption: best.title } } },
				],
			},
		],
	}),
});

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);
}
  1. POST /clips/info shows the cost before spending.
  2. POST /clips with template_id already delivers each clip edited.
  3. The highest-scoring clip comes from sort=rating&order=desc&limit=1.
  4. from_cache: true means the MP4 already existed.
  5. The same edit_id goes to both accounts, each with its network's metadata.

The explicit render is optional: the post renders what is missing. It is here so you have the MP4 before publishing.

A post can end in partial: one account published and the other failed. Look item by item and use POST /posts/{id}/items/{itemId}/retry on the one that failed. What is already live is not touched.

A queue of several videos

Clipping a whole channel is the same flow repeated. The care is not to fire everything at once: polling dozens of submissions together is what hits the rate limit. A pipeline with a few workers solves it. It uses call and until from the previous recipe.

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), CLIPPING, 15);
	if (done.status === "failed") return { url, error: done.error_code };
	const { clips } = await call(`${base}/clips`);
	return { url, clips };
}

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

Before the batch, add up the credits_cost of each POST /clips/info and compare with GET /balance. A submission stuck in downloading with waiting_reason filled in is waiting for the source (premiere, live stream on air, recording being processed) and continues on its own. See A source that is not live yet.

The best moments of a live stream, posted automatically

A monitor with template_id delivers each clip of the live stream already edited. This loop checks the live stream every minute and publishes every new clip with a score of 8 or higher. It uses call from the first recipe.

const MONITOR_ID = "7413020011876610";
const TIKTOK = "7390012299881100";
const posted = new Set();

while (true) {
	const { broadcasts } = await call(`/monitors/${MONITOR_ID}/broadcasts?limit=1`);
	const live = broadcasts[0];

	if (live && !live.recording_expired) {
		const { clips } = await call(`/monitors/${MONITOR_ID}/broadcasts/${live.id}/clips?min_rating=8&limit=100`);
		const fresh = clips.filter((clip) => clip.edit_id && !posted.has(clip.id));

		for (const clip of fresh) {
			await call("/posts", {
				method: "POST",
				body: JSON.stringify({
					videos: [{ edit_id: clip.edit_id, targets: [{ connection_id: TIKTOK, metadata: { tiktok: { title: clip.title, privacyLevel: "PUBLIC_TO_EVERYONE" } } }] }],
				}),
			});
			posted.add(clip.id);
		}
	}

	await new Promise((resolve) => setTimeout(resolve, 60_000));
}
  • The live stream generates clips in 10-minute blocks while it is on air. Checking every minute is more than enough.
  • posted lives in memory. In production, store the published clip.id values in your database so you do not repost after a restart.
  • The filter skips clips without edit_id. To post those too, render with POST .../clips/{clipId}/render and use the edit_id from the response.
  • The monitor charges per 10-minute block that generated clips while it is on. See Monitor channels and live streams.

Publishing automatically posts to the person's account without review. Start with privacyLevel: "SELF_ONLY" on TikTok, check the first results and only then open it to everyone.

On this page