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);
}POST /clips/infoshows the cost before spending.POST /clipswithtemplate_idalready delivers each clip edited.- The highest-scoring clip comes from
sort=rating&order=desc&limit=1. from_cache: truemeans the MP4 already existed.- The same
edit_idgoes to both accounts, each with its network'smetadata.
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.
postedlives in memory. In production, store the publishedclip.idvalues in your database so you do not repost after a restart.- The filter skips clips without
edit_id. To post those too, render withPOST .../clips/{clipId}/renderand use theedit_idfrom 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.