← Cut to Match
API
Tokens

Drive Cut to Match from your own code

The one metered task, match, is available over HTTP. It takes a storyboard of two to eight shots - what is in frame, its shape, its place on a 3 by 3 frame grid, its main colour, how it starts and ends, its speed and length - together with the page's read of every cut, and returns a verdict, one filming recipe per cut (how shot A ends, how shot B starts, where to cut, a fix where one is needed), two to four shooting tips and what already works.

The division of labour first: the page owns the checks; the model owns the words; the page re-checks the reply. Working out what carries across each cut, choosing its seam, the cut verdicts, the sequence findings and the overall verdict are plain arithmetic in cutkit.js; send them in, and check the reply with CutKit.check.

Get a token on the token page (step 2 below also shows how to mint a guest token in code).

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"cut-to-match"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object - no wrapper.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, aspect, shots, cuts, facts, figures) is missing or not a string.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the recipes are metered and need a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field and the verdict rule

Every run sends task: match (the only task; a missing or unknown one is treated as match). Each cut is locked (two or more of motion, shape, place in frame and colour carry across, nothing clashing), loose (one carries across) or broken (nothing does, the motion doubles back, or the zoom flips). The verdict is a fixed rule on the page's figures, checked in this order:

  1. rethink - 1 or more broken cuts.
  2. tighten - 1 or more gaps (a loose cut, a slow side on a motion cut, a motion cut travelling against an earlier one, a pull-back before the last cut, too many rack-focus cuts).
  3. ready - everything else.

The declared input schema carries scalars only, so the array- and object-valued fields travel JSON-encoded as strings (CutKit.toWire does this). Every value is a string.

Input fields

FieldTypeNotes
taskstringmatch. Required.
ideastringWhat the video is, up to 140 characters. Optional.
aspectstringvertical 9:16, wide 16:9 or square 1:1. Required.
shotsstring (JSON)[{id, name, shape, place, colour, starts, ends, speed, seconds}] - ids S1.., 2 to 8, plain words. Required.
cutsstring (JSON)[{id, from, to, shared, seam, seam_name, verdict, spec}] - ids C1..; seam is one of whip, zoom, pullback, focus, graphic, plain. Required.
factsstring (JSON)[{id, level, ref, text}] - ids F1..; level is break or gap; ref is a cut id or ALL. Required (may be []).
figuresstring (JSON){shots, cuts, locked, loose, broken, gaps, seconds}, every value a string. Required.
notesstringFree text about the shoot, up to 300 characters.
clip_note, retry_notestringWhat the page cut, when it cut something; and only on a reformat retry, what was wrong.

The reply

The job's output.output is a string holding one JSON object: task, verdict, title, summary, cuts (one {cut, seam, verdict, end_a, start_b, on_the_cut, fix} per input cut, in order, with the page's seam and verdict; fix is empty only for a locked cut no finding names), tips (2-4 strings) and keep. Parse it with CutKit.parseReply (which also closes a cut-off stream) and CutKit.normalize.

Worked examples

Replies produced against the app's prompt for the three sample storyboards (made up; long fields shortened; the full replies ship in example-result.js). Together they cover every verdict.

a three-shot morning where every cut locks - ready

Input:

{
 "task": "match",
 "idea": "A slow morning in three round things",
 "aspect": "vertical 9:16",
 "shots": "[{\"id\":\"S1\",\"name\":\"Alarm clock on the nightstand\",\"shape\":\"round\",\"place\":\"centre\",\"colour\":\"white\",\"starts\":\"starts holding still\",\"ends\":\"ends pushing in, toward the lens\",\"speed\":\"fast\",\"seconds\":2},{\"id\":\"S2\",\"name\":\"Coffee mug from above\",\"shape\":\"round\",\"place\":\"centre\",\"colour\":\"white\",\"starts\":\"starts pushing in, toward the lens\",\"ends\":\"ends moving right\",\"speed\":\"fast\",\"seconds\":2},{\"id\":\"S3\",\"name\":\"Bike  ... (573 characters)",
 "cuts": "[{\"id\":\"C1\",\"from\":\"S1\",\"to\":\"S2\",\"shared\":[\"motion\",\"shape\",\"place in frame\",\"colour\"],\"seam\":\"zoom\",\"seam_name\":\"Zoom-through\",\"verdict\":\"locked\",\"spec\":\"Both sides push in. Shot A grows toward the lens for about the last 0.2 s before the cut; cut at its fastest, blurriest moment; shot B keeps growing into focus for about 0.5 s after the cut. Use it to go deeper into the same idea.\"},{\"id\":\"C2\",\"from\":\"S2\",\"to\":\"S3 ... (794 characters)",
 "facts": "[]",
 "figures": "{\"shots\":\"3\",\"cuts\":\"2\",\"locked\":\"2\",\"loose\":\"0\",\"broken\":\"0\",\"gaps\":\"0\",\"seconds\":\"7\"}",
 "notes": "Shot on one phone, handheld. The mug is filmed on the kitchen table.",
 "clip_note": ""
}

Reply:

{
 "task": "match",
 "verdict": "ready",
 "title": "Both cuts lock - you're ready to shoot",
 "summary": "Ready: 2 cuts are locked, 0 are loose, and 0 are broken. Shoot it handheld, keeping the push-in and the rightward motion smooth throughout.",
 "cuts": [
  {
   "cut": "C1",
   "seam": "zoom",
   "verdict": "locked",
   "end_a": "Hold the alarm clock centred in the frame, white and round, pushing in toward the lens as it builds to its fastest, blurriest moment just before the cut.",
   "start_b": "Start the mug already pushing in toward the lens, the same white, round shape centred in the frame, growing into focus for about 0.5 s after the cut.",
   "on_the_cut": "Cut at the alarm clock's fastest, blurriest moment, right before the mug picks up the same push in.",
   "fix": ""
  },
  {
   "cut": "C2",
   "seam": "whip",
   "verdict": "locked",
   "end_a": "Have the mug accelerate moving right, travelling about 12% of the frame as it builds to peak speed before the cut.",
   "start_b": "Pick up the wheel already moving right at the same speed, carrying the motion on for roughly 0.3 s before it settles.",
   "on_the_cut": "Cut mid-motion at peak speed, keeping the movement inside the frame on both sides of the cut.",
   "fix": ""
  }
 ],
 "tips": [
  "Keep the phone steady through each push-in so the zoom-through reads as one continuous move.",
  "Match the rightward speed on both sides of the cut so the motion carries straight through.",
  "Since you're filming handheld, brace your arms to keep both moves smooth and steady."
 ],
 "keep": "The first cut carries four things across at once - motion, shape, place in frame and colour - a strong match."
}

four outfits in one mirror with a loose cut, a slow side and a change of direction - tighten

Input:

{
 "task": "match",
 "idea": "Four outfits, one hallway mirror",
 "aspect": "vertical 9:16",
 "shots": "[{\"id\":\"S1\",\"name\":\"Hoodie and jeans in the mirror\",\"shape\":\"tall and upright\",\"place\":\"centre\",\"colour\":\"black\",\"starts\":\"starts holding still\",\"ends\":\"ends moving left\",\"speed\":\"fast\",\"seconds\":2},{\"id\":\"S2\",\"name\":\"Green dress, same mirror\",\"shape\":\"tall and upright\",\"place\":\"centre\",\"colour\":\"green\",\"starts\":\"starts moving left\",\"ends\":\"ends holding still\",\"speed\":\"fast\",\"seconds\":2},{\"id\":\"S3\",\"name\":\"Hand grabb ... (782 characters)",
 "cuts": "[{\"id\":\"C1\",\"from\":\"S1\",\"to\":\"S2\",\"shared\":[\"motion\",\"shape\",\"place in frame\"],\"seam\":\"whip\",\"seam_name\":\"Cut the curve\",\"verdict\":\"locked\",\"spec\":\"Both shots move the same way at speed. Shot A accelerates out, travelling about 12% of the frame; cut mid-motion at peak speed; shot B carries on the same way and settles. Roughly 0.3 s on each side. Keep partial travel - nothing needs to leave the frame.\"},{\"id\":\"C2\",\"fr ... (983 characters)",
 "facts": "[{\"id\":\"F1\",\"level\":\"gap\",\"ref\":\"C2\",\"text\":\"Only the place in frame carries from Green dress, same mirror to Hand grabbing a raincoat.\"},{\"id\":\"F2\",\"level\":\"gap\",\"ref\":\"C3\",\"text\":\"The motion cut from Hand grabbing a raincoat to Raincoat on, walking to the door (cut the curve) has a slow side - a motion cut hides best at peak speed, so both sides should move fast.\"},{\"id\":\"F3\",\"level\":\"gap\",\"ref\":\"C3\",\"text\":\"The cu ... (647 characters)",
 "figures": "{\"shots\":\"4\",\"cuts\":\"3\",\"locked\":\"2\",\"loose\":\"1\",\"broken\":\"0\",\"gaps\":\"3\",\"seconds\":\"8\"}",
 "notes": "The mirror is in a narrow hallway, so there is not much room to step back.",
 "clip_note": ""
}

Reply:

{
 "task": "match",
 "verdict": "tighten",
 "title": "Two cuts need tightening before you shoot",
 "summary": "This storyboard needs tightening: 2 cuts lock, 1 is loose, and 0 are broken. Start by giving the cut from the green dress shot to the raincoat hand shot a second shared thing.",
 "cuts": [
  {
   "cut": "C1",
   "seam": "whip",
   "verdict": "locked",
   "end_a": "Keep the upright hoodie and jeans shot centred, then move it left fast as it exits, travelling about 12% of the frame.",
   "start_b": "Pick up the green dress shot already centred and moving left at the same fast speed, then let it settle.",
   "on_the_cut": "Cut mid-motion at peak speed, about 0.3 s into each shot's movement.",
   "fix": ""
  },
  {
   "cut": "C2",
   "seam": "plain",
   "verdict": "loose",
   "end_a": "End the green dress shot drifting left rather than holding still, so a move carries into the next shot.",
   "start_b": "Start the raincoat hand shot already drifting left too, instead of holding still, so the move continues.",
   "on_the_cut": "Cut where both shots' leftward drift and framing line up.",
   "fix": "Give both still ends the same move: have the green dress shot and the raincoat hand shot drift left instead of holding still."
  },
  {
   "cut": "C3",
   "seam": "whip",
   "verdict": "locked",
   "end_a": "Have the raincoat hand shot accelerate left fast at the end, travelling about 12% of the frame, instead of right.",
   "start_b": "Pick up the walking shot already moving left at the same fast speed, then let it settle.",
   "on_the_cut": "Cut mid-motion at peak speed, about 0.3 s into each shot's movement.",
   "fix": "Make the walking shot's slow side fast, and turn its direction around so both shots at this cut travel left instead of right."
  }
 ],
 "tips": [
  "Film both sides of a whip cut back to back so the speed and direction match exactly.",
  "Keep one direction of travel through the whole video, not just within a single cut.",
  "With no room to step back in the hallway, use a small sideways move instead."
 ],
 "keep": "The cut from the hoodie and jeans shot to the green dress shot already locks, with motion, shape and place in frame all carrying across."
}

a weekend in four shots where one zoom flips at the cut - rethink

Input:

{
 "task": "match",
 "idea": "A Saturday in four shots",
 "aspect": "vertical 9:16",
 "shots": "[{\"id\":\"S1\",\"name\":\"Pancake flip\",\"shape\":\"round\",\"place\":\"centre\",\"colour\":\"yellow\",\"starts\":\"starts holding still\",\"ends\":\"ends moving up\",\"speed\":\"fast\",\"seconds\":2},{\"id\":\"S2\",\"name\":\"Tennis ball toss\",\"shape\":\"round\",\"place\":\"top centre\",\"colour\":\"yellow\",\"starts\":\"starts moving up\",\"ends\":\"ends pulling back, away from the lens\",\"speed\":\"fast\",\"seconds\":2},{\"id\":\"S3\",\"name\":\"Friends waving from the bench\",\"shape ... (777 characters)",
 "cuts": "[{\"id\":\"C1\",\"from\":\"S1\",\"to\":\"S2\",\"shared\":[\"motion\",\"shape\",\"colour\"],\"seam\":\"whip\",\"seam_name\":\"Cut the curve\",\"verdict\":\"locked\",\"spec\":\"Both shots move the same way at speed. Shot A accelerates out, travelling about 12% of the frame; cut mid-motion at peak speed; shot B carries on the same way and settles. Roughly 0.3 s on each side. Keep partial travel - nothing needs to leave the frame.\"},{\"id\":\"C2\",\"from\":\"S2\" ... (750 characters)",
 "facts": "[{\"id\":\"F1\",\"level\":\"break\",\"ref\":\"C2\",\"text\":\"Tennis ball toss ends pulling back, away from the lens, but Friends waving from the bench starts pushing in, toward the lens - the zoom flips direction at the cut.\"},{\"id\":\"F2\",\"level\":\"gap\",\"ref\":\"C3\",\"text\":\"Only the shape carries from Friends waving from the bench to Sunset over the car park. The two subjects sit in neighbouring parts of the frame - one nudge would li ... (434 characters)",
 "figures": "{\"shots\":\"4\",\"cuts\":\"3\",\"locked\":\"1\",\"loose\":\"1\",\"broken\":\"1\",\"gaps\":\"1\",\"seconds\":\"11\"}",
 "notes": "Everything is already filmed except the sunset.",
 "clip_note": ""
}

Reply:

{
 "task": "match",
 "verdict": "rethink",
 "title": "One broken cut and one loose cut - rethink first",
 "summary": "This storyboard needs a rethink: 1 cut is locked, 1 is loose, and 1 is broken. Fix the broken cut between the tennis ball toss and the friends waving from the bench first, since the zoom flips direction there.",
 "cuts": [
  {
   "cut": "C1",
   "seam": "whip",
   "verdict": "locked",
   "end_a": "End the pancake flip moving up at speed, travelling about 12% of the frame, so the motion is still accelerating as you cut.",
   "start_b": "Start the tennis ball toss moving up at the same fast speed as the pancake flip, carrying the motion straight on.",
   "on_the_cut": "Cut mid-motion at peak speed, keeping roughly 0.3 s of movement on each side, with partial travel so nothing leaves frame.",
   "fix": ""
  },
  {
   "cut": "C2",
   "seam": "plain",
   "verdict": "broken",
   "end_a": "End the tennis ball toss pulling back, away from the lens, keeping the ball centred near the top of frame as it recedes.",
   "start_b": "Start the friends waving shot pulling back too, away from the lens, so the bench group recedes the same way the ball did.",
   "on_the_cut": "Cut when both shots are pulling back at the same speed, so the movement continues smoothly across the edit.",
   "fix": "Turn the friends waving shot's start around to pull back, away from the lens, matching the tennis ball toss's ending zoom."
  },
  {
   "cut": "C3",
   "seam": "plain",
   "verdict": "loose",
   "end_a": "End the friends waving from the bench shot wide and flat in the bottom centre of frame, letting the group go soft as it drifts out of focus.",
   "start_b": "Start the sunset over the car park shot wide and flat, holding still, with the car park framed in the bottom centre to match where the friends sat.",
   "on_the_cut": "Cut once both shapes read as wide and flat and the frame placement lines up, so the edit feels like one continuous view.",
   "fix": "Frame the sunset shot in the bottom centre too, matching where the friends waving from the bench sit, to line up the cut."
  }
 ],
 "tips": [
  "Keep one direction of travel on a cut the curve seam so the motion never doubles back.",
  "Match the direction of a zoom across a cut so it never flips midway.",
  "Since the sunset is still unfilmed, frame it to match the friends waving shot's place first."
 ],
 "keep": "The pancake flip into tennis ball toss cut already carries motion, shape, and colour together."
}

Build the input yourself

Load the script in node (global.window = global; require("./cutkit.js")), then:

const K = CutKit;
const form = K.form({ idea: "Door to door", aspect: "9:16", notes: "",
  shots: [{ name: "Front door opening", shape: "tall", cell: "c", colour: "red", in: "still", out: "right", speed: "fast", secs: 2 },
          { name: "Garden gate opening", shape: "tall", cell: "c", colour: "green", in: "right", out: "still", speed: "fast", secs: 2 }] });
const r = K.read(form);                            // r.cuts (shared handles, seam, verdict), r.facts (breaks and gaps), r.fig (counts, verdict)
const built = K.buildInput(form);
const wire = K.toWire(built.input);               // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes                    // the page's re-check of the reply
// K.md(reply, built, form); K.csv(built)         // the Markdown storyboard and the CSV shot list

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://cut-to-match.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"cut-to-match"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "match", "idea": "A slow morning in three round things", "aspect": "vertical 9:16", "shots": "[{\"id\":\"S1\",\"name\":\"Alarm clock on the nightstand\",\"shape\":\"round\",\"place\":\"centre\",\"colour\":\"white\",\"starts\":\"starts holding still\",\"ends\":\"ends pushing in, toward the lens\",\"speed\":\"fast\",\"seconds\":2}, ...]", "cuts": "[...]", "facts": "[]", "figures": "{\"shots\":\"3\",\"cuts\":\"2\",\"locked\":\"2\",\"loose\":\"0\",\"broken\":\"0\",\"gaps\":\"0\",\"seconds\":\"7\"}", "notes": "Shot on one phone, handheld. The mug is filmed on the kitchen table."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses cut-to-match:<task>:<inputHash>:a<attempt> (the one task is match), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="cut-to-match:match:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: cut-to-match:match:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json