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

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.