Docs.
Most people should just use the studio. This page is for wiring Cutlist into your own product: submit jobs over HTTP and get a signed callback when the clips are ready.
Timestamps you already have
If your product already records when things happen - a play log, chapter markers, agenda items - send those instead of paying a model to infer them. Detection is skipped entirely and the job bills at the anchored rate, with the start-up charge down to the delivery and the renders.
{
"source": "https://cdn.example.com/game.mp4",
"anchors": [
{
"start": 1421.5,
"end": 1452.0,
"title": "42-yard touchdown"
}
]
}Studio users get the same thing by pasting chapters into the upload box. Only start is required.
The API
Renders take minutes, so the API is asynchronous: submit a job, then poll it or wait for the callback. Authenticate with your project key as a bearer token. A bad or missing key returns 404 rather than 401, so probing cannot confirm which routes exist.
Submit a job
curl -X POST https://cutlist.app/api/v1/jobs \ -H "Authorization: Bearer clk_live_..." \ -H "Content-Type: application/json" \ -d '{ "source": "https://cdn.example.com/episode.mp4", "options": { "numClips": 5, "aspect": "9:16" }, "callbackUrl": "https://you.app/hooks/cutlist", "metadata": { "episodeId": "ep_412" } }'
Returns 202 with the job. Poll GET /api/v1/jobs/:id for status, or list with GET /api/v1/jobs?status=done&limit=20. An unfinished job can be stopped with POST /api/v1/jobs/:id/cancel.
What source can be
A direct media URL, the urlyou get back from finalizing an upload, or the page a video sits on — paste a watch page and the worker opens it and fetches the video behind it. A page cannot be measured until it is opened, so a job submitted with one and no durationSecondsis held at the nominal 600 seconds and re-quoted before any billable stage runs. Some things will not come through that way and are refused by name rather than guessed at: a live stream, a playlist or channel URL, and anything a signed-out visitor cannot see, because the tool signs in to nothing. The rights to whatever is at the other end are yours to hold — that is a rights question and the tool cannot answer it for you.
Job status
queued → claimed → processing → done. Terminal states are done, failed and canceled. A job whose worker dies is returned to the queue automatically and retried up to three times.
Options
| Field | Default | Notes |
|---|---|---|
numClips | 5 | 1-20 clips returned, ranked by score. |
aspect | "9:16" | Also "1:1" and "16:9". |
layout | "frame" | frame keeps the whole picture on a blurred bed. track follows the action, crop centres a face, center is a straight crop. |
minLen / maxLen | 15 / 90 | Seconds. Clip bounds are snapped to sentence ends where a transcript exists. |
targetLen | 45 | What the selector aims for between the bounds. |
mode | "auto" | auto picks talk or action from how much speech it finds. |
captions | true | Word-timed subtitles burned in, plus an SRT sidecar. |
captionStyle | "bold" | Also "punch" (one huge word) and "clean" (quieter). |
accent | "#d62828" | Active-word colour in the captions. |
prompt | none | Constrains selection, e.g. every lead change. |
whisperModel | "large-v3" | tiny through large-v3. Smaller is faster and a small credit discount. |
numClips | 5 | 1-20. Moves the price: the review sees three candidate windows per clip. |
Anchors
If you already know where the moments are, send them. Cutlist skips detection entirely - the engine builds its model clients with a budget of zero calls, so there is no sweep, no grounded review and no ranking pass - so the start-up charge falls to what packaging still costs: 0.75 for delivery plus 0.05 a clip for the renders, which is 1 credit on a five-clip job. It runs reframe, caption and render against the timestamps you supplied. Captions still need a transcript, so if you leave them on, Whisper still runs over the source and is billed.
{
"source": "https://cdn.example.com/game.mp4",
"anchors": [
{
"start": 1421.5,
"end": 1452.0,
"score": 95,
"title": "Nolan 42-yd TD",
"category": "touchdown"
}
]
}Only start is required. Anchors are trusted for where a moment is but still clamped to the source and to your length bounds, so a timestamp past the end of the recording cannot produce a broken clip.
Webhooks
When a job reaches a terminal state, Cutlist POSTs the same body you would get from GET /api/v1/jobs/:id to your callback URL, signed with your project secret.
X-Cutlist-Event: job.done X-Cutlist-Delivery: job_01hq... X-Cutlist-Signature: sha256=9f2c...
// verify against the RAW body, before parsing const expected = "sha256=" + crypto.createHmac("sha256", secret).update(raw).digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return new Response("bad signature", { status: 400 }); }
Delivery is best effort and never retried into your error budget: if your endpoint is down the job still succeeded, and polling returns the identical payload.
What happens after you submit
Rendering runs on our infrastructure. A job is queued, claimed by a render worker, and held under a lease it has to keep renewing; if that worker dies the lease lapses and the job returns to the queue on its own, so a failed machine cannot strand your work.
You never operate any of that. Poll the job or take the webhook — both return the identical payload, and a job that never produced clips is refunded in full rather than billed for our failure.
Wiring it into a stats platform
This is what anchors were built for. If your platform already records when things happened, you never need Cutlist to guess.
// after a play is confirmed, ship it as a short await fetch("https://cutlist.app/api/v1/jobs", { method: "POST", headers: { Authorization: `Bearer ${process.env.CUTLIST_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ source: broadcast.recordingUrl, anchors: plays .filter((p) => p.weight >= 75) .map((p) => ({ start: p.startSec, end: p.startSec + p.durationSec, score: p.weight, title: p.summary, category: p.playType, })), callbackUrl: "https://statside.app/api/cutlist/callback", metadata: { gameId: game.id }, }), });
The callback carries your metadata back untouched, so the handler knows which game the clips belong to without keeping state.