CUTLIST
N°00Reference

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 are slow — budget roughly the length of the source itself, so an hour of video takes about an hour and a three-hour file about three. 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.

It can also be no longer than 3 hours. Send a durationSeconds past that and the submit answers 400 source_too_longwithout creating anything; leave it out and the job is created, claimed, and refused by the admission gate on the length the worker measures, with the hold released in full and nothing charged. Split a longer recording into parts and submit each part as its own job — the meter prices each on its own length.

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

FieldDefaultNotes
numClips51-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 / maxLen15 / 90Seconds. Clip bounds are snapped to sentence ends where a transcript exists.
targetLen45What the selector aims for between the bounds.
mode"auto"auto picks talk or action from how much speech it finds.
captionstrueWord-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.
promptnoneConstrains selection, e.g. every lead change.
whisperModel"large-v3"tiny through large-v3. Smaller is faster and a small credit discount.
numClips51-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.