← Cue Scroll / API
Tokens

Drive Cue Scroll from your own code

Everything the page asks the coach is available over HTTP. A natural use is a set of talks kept as text - a weekly sermon, a lesson series, a stack of conference talks - where a script sends each one with its target time, gets back where to pause, breathe and lean on a word, applies the cuts it accepts, and after a rehearsal sends only the paragraphs the speaker still rushes or drags.

One thing to be clear about first: the model never times anything and never rewrites the speech. The caller splits the script into paragraphs, counts the words and computes the planned seconds at the chosen pace (pauses and slowed words included), and measures the rehearsal. The model returns marks, cuts and advice; the caller checks every paragraph id, every word of every mark-up and every cut against its own script and recounts the time before applying anything. The page's timing and checking code ships as a plain script you can load in node — see the end of the examples.

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":"cue-scroll"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}} — that returns 200, warns unknown field 'input', and hides every field from the model.

Errors:

StatusCodeMeaning
400validation_errorThe body is not a JSON object.
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: coaching is metered and needs 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 comes first

One system prompt serves two tasks (lanes). task picks the contract; a missing or unknown task makes the model do mark and say so in the reply's lane_note.

taskWhenExtra fieldsWorked example
markMark up every paragraph sent in full, name the arrow (the one sentence the audience must remember), write how to open and close, and propose verbatim trims when planned_seconds is over target_seconds. With a rehearsal, also pacing advice for each rushed or dragged paragraph.Optional rehearsal.toast (over target, trims), lesson (fits, slides)
remarkAfter a rehearsal: re-mark only the targets, one pacing entry per target, no trims unless the plan is still more than 10% over the target, and a one-line open/close reminder unless a target is the first or last paragraph.targets (required for this task) and rehearsal.eulogy

Every run body is one JSON object of scalar fields: strings, numbers and booleans only, so the two structured fields travel as JSON-encoded strings (encode the array, then send the resulting string as the field's value). The page always sends every field below except retry_note.

FieldWhat it holds
task"mark" or "remark".
titleThe speech's title, up to 120 characters ("Untitled speech" when empty).
occasionOne of toast, eulogy, keynote, lesson, sermon, presentation, pitch, other.
target_secondsNumber: the time the speech must hit (the page allows 15 to 7200).
wpmNumber: the speaking pace, 80 to 220 words per minute.
slidesBoolean: true when they speak with slides, so the reply places [cue: slide ...] marks where slides change.
planned_secondsNumber: the whole script at wpm, computed by the caller with existing marks counted (see the timings below).
paragraphsJSON-encoded string: [{"id":"p1","words":42,"secs":18.0,"full":true,"text":"..."}]. Ids are p1..pn in order and are the only ids the reply may use. text may already carry marks. Over 16,000 characters of script, the first two, the last two, the targets and the rushed or dragged paragraphs stay full and the rest are cut to their first 30 words, ending […N more words], with "full":false: those get no marks and no trims.
rehearsal"", or a JSON-encoded string: [{"id":"p3","planned_secs":20.1,"actual_secs":14.2,"pace":"rush"}]. The page calls a paragraph rush under 0.88 of its planned time and drag over 1.12 (and at least one second off), else ok.
targetsremark only: comma-separated ids, "p2,p5,p7"; "" for mark.
clipped"", or a note on what was shortened before sending.
retry_noteSent only on a reformat retry, when the previous reply was not one valid JSON object: says what was wrong. Leave it out otherwise.

The mark grammar

The only marks the app understands, written into the paragraph's own text as separate tokens between words. The same grammar is accepted in the text you send and comes back in each marked.

MarkMeaningPlanned time
/A short pause, a beat: and then / she laughed.+0.6 s
//A long pause: around the arrow, after a punchline, before the last line.+1.6 s
^A breath point inside a long sentence.+0.4 s
*words*Emphasis: lean on one to four words, at most two per paragraph.none
[slow]Slow down from here to the end of the sentence (the arrow, numbers, names, the close).words about 18% longer
[cue: text]A stage or slide cue said by nobody: [cue: raise glass], [cue: slide 4 - the map].none

The reply

The reply text is one JSON object, every key present even when empty:

{ "task": "mark|remark", "lane_note": "",
  "arrow": "the one sentence the audience must remember, from the script",
  "marks":  [{ "id": "p1", "marked": "the paragraph's exact words with marks inserted", "note": "why these marks" }],
  "trims":  [{ "id": "p4", "cut": "an exact, contiguous run of words from that paragraph", "words_saved": 23, "reason": "why it can go" }],
  "pacing": [{ "id": "p3", "issue": "rush|drag|ok", "advice": "one concrete instruction" }],
  "open": "how to open", "close": "how to land the close",
  "seconds_saved": 31 }

trims are ordered from the least costly cut to the most, and are [] with seconds_saved 0 when the script already fits. A cut never removes the arrow or the last paragraph's final sentence.

Check before you apply. This is what the page does with every reply:

Worked examples

These are the bundled examples on the page: the exact run body the page builds and a reply produced against the shipped prompt (the page replays the same replies for free). One example per lane: two for mark, one for remark.

mark: a best man's toast that runs 23 seconds over its target

Input (planned_seconds 158 against target_seconds 135, so the reply must trim; no rehearsal yet):

{
 "task": "mark",
 "title": "Best man toast for Tom and Priya",
 "occasion": "toast",
 "target_seconds": 135,
 "wpm": 140,
 "slides": false,
 "planned_seconds": 158,
 "paragraphs": "(a JSON string - decoded below)",
 "rehearsal": "",
 "targets": "",
 "clipped": ""
}

paragraphs, decoded (send it re-encoded as one string):

[
 {
  "id": "p1",
  "words": 45,
  "secs": 19.3,
  "full": true,
  "text": "Good evening, everyone. For those who don't know me, I'm Sam, and I have had the honour of being Tom's best friend for twenty-two years, which, as Priya pointed out this morning, is longer than some of the wine on these tables has been alive."
 },
 {
  "id": "p2",
  "words": 42,
  "secs": 18,
  "full": true,
  "text": "Before I start, I want to thank Priya's parents, Anil and Meera, for this wonderful evening, and Tom's mum, Linda, who has been calling me every day for three weeks to make sure this speech is clean. Linda, it is mostly clean."
 },
 {
  "id": "p3",
  "words": 66,
  "secs": 28.3,
  "full": true,
  "text": "Tom and I met when we were ten. We decided to build a raft out of fence panels and a paddling pool and sail it across Miller's pond. It sank in about four seconds. I remember standing in the water up to my chest, and Tom, completely soaked, turned to me and said, very seriously, that the design was fine and the pond was the problem."
 },
 {
  "id": "p4",
  "words": 56,
  "secs": 24,
  "full": true,
  "text": "That is Tom. He has never once in his life admitted that a plan was bad. He just finds a different pond. He did it with his first business, he did it with the time he tried to learn the bagpipes, and he did it, I am told, with three separate attempts at a sourdough starter."
 },
 {
  "id": "p5",
  "words": 65,
  "secs": 27.9,
  "full": true,
  "text": "And then, four years ago, at a pub quiz in Leeds, he met Priya. Now, Tom is a quiz machine. He has not lost a pub quiz since 2014. But that night, when the question was what is the capital of France, he wrote down Lyon. On purpose. Because Priya's team was one point behind and he wanted an excuse to go over and apologise."
 },
 {
  "id": "p6",
  "words": 55,
  "secs": 23.6,
  "full": true,
  "text": "Priya told me later that she knew exactly what he was doing from the very first question. Nobody gets Paris wrong. She just thought it was sweet. And that, I think, is the whole marriage in one sentence: he makes a grand, slightly ridiculous gesture, and she sees straight through it and loves him anyway."
 },
 {
  "id": "p7",
  "words": 30,
  "secs": 12.9,
  "full": true,
  "text": "Tom, you have always said the design was fine and the pond was the problem. Well, today you found the right pond. Priya, thank you for seeing straight through him."
 },
 {
  "id": "p8",
  "words": 10,
  "secs": 4.3,
  "full": true,
  "text": "So please, everyone, raise your glasses. To Tom and Priya."
 }
]

Reply:

{
 "task": "mark",
 "lane_note": "",
 "arrow": "He makes a grand, slightly ridiculous gesture, and she sees straight through it and loves him anyway.",
 "marks": [
  {
   "id": "p1",
   "marked": "Good evening, everyone. / For those who don't know me, I'm Sam, and I have had the honour of being Tom's best friend for *twenty-two years*, which, as Priya pointed out this morning, is longer than some of the wine on these tables has been alive.",
   "note": "One beat after the greeting so the room settles; lean on the twenty-two years."
  },
  {
   "id": "p2",
   "marked": "Before I start, I want to thank Priya's parents, Anil and Meera, for this wonderful evening, and Tom's mum, Linda, who has been calling me every day for three weeks to make sure this speech is clean. / Linda, it is mostly clean.",
   "note": "A beat before the line to Linda lets the joke land; look at her as you say it."
  },
  {
   "id": "p3",
   "marked": "Tom and I met when we were ten. We decided to build a raft out of fence panels and a paddling pool and sail it across Miller's pond. / It sank in about four seconds. / I remember standing in the water up to my chest, and Tom, completely soaked, turned to me and said, very seriously, ^ that the design was fine and *the pond* was the problem.",
   "note": "Pause around the sinking for the laugh, then breathe before Tom's line; it comes back at the end."
  }
 ],
 "trims": [
  {
   "id": "p1",
   "cut": "For those who don't know me,",
   "words_saved": 6,
   "reason": "Throat-clearing; 'I'm Sam' does the same job."
  },
  {
   "id": "p2",
   "cut": "for this wonderful evening,",
   "words_saved": 4,
   "reason": "The thanks stands without the description."
  },
  {
   "id": "p2",
   "cut": "every day for three weeks",
   "words_saved": 5,
   "reason": "The joke works without the detail."
  }
 ],
 "pacing": [],
 "open": "Stand still, find Tom and Priya, take one breath before speaking, then say 'Good evening, everyone' and pause before giving your name.",
 "close": "After 'raise your glasses', raise yours and wait two seconds, then say 'To Tom and Priya' slowly and drink. Add nothing after it.",
 "seconds_saved": 35.6
}

Shortened here: 5 more marks (p4, p5, p6, p7, p8); 6 more trims (p3, p5, p1, p3, p4, p6) left out. The page replays the whole reply.

mark: a lesson opening that fits, spoken with slides

Input (slides is true, so the reply puts a [cue: ...] where each slide changes; the script fits, so trims must be empty):

{
 "task": "mark",
 "title": "Why does the Moon change shape?",
 "occasion": "lesson",
 "target_seconds": 135,
 "wpm": 135,
 "slides": true,
 "planned_seconds": 119,
 "paragraphs": "(a JSON string - decoded below)",
 "rehearsal": "",
 "targets": "",
 "clipped": ""
}

paragraphs, decoded (send it re-encoded as one string):

[
 {
  "id": "p1",
  "words": 46,
  "secs": 20.4,
  "full": true,
  "text": "Hands up if you have ever looked at the Moon and thought it was following you home. Keep them up if you have ever noticed it is a different shape on different nights. Right. Nearly all of you. Today we are going to work out why."
 },
 {
  "id": "p2",
  "words": 44,
  "secs": 19.6,
  "full": true,
  "text": "Here is the strange part. The Moon does not make any light of its own. Not one bit. Everything you have ever seen of the Moon is sunlight, bouncing off grey rock and travelling about four hundred thousand kilometres to land in your eye."
 },
 {
  "id": "p3",
  "words": 50,
  "secs": 22.2,
  "full": true,
  "text": "So at any moment, half of the Moon is lit by the Sun and half of it is in darkness, exactly like the Earth has a day side and a night side. That never changes. What changes is how much of the lit half we can see from down here."
 },
 {
  "id": "p4",
  "words": 74,
  "secs": 32.9,
  "full": true,
  "text": "Look at this picture. The Moon goes around the Earth about once every twenty-nine and a half days. When it sits between us and the Sun, the lit half faces away from us and we see nothing. That is a new moon. When it is on the far side of us, we see the whole lit face. That is a full moon. Everything in between is a crescent, a half or a gibbous moon."
 },
 {
  "id": "p5",
  "words": 53,
  "secs": 23.6,
  "full": true,
  "text": "In a minute you are going to prove this yourselves with a lamp, a ball on a stick and your own head. But first, one question to keep in your mind for the whole lesson: if the Moon were the same shape every night, what would that tell us about where it is?"
 }
]

Reply:

{
 "task": "mark",
 "lane_note": "",
 "arrow": "What changes is how much of the lit half we can see from down here.",
 "marks": [
  {
   "id": "p1",
   "marked": "[cue: slide 1 - the Moon at night] Hands up if you have ever looked at the Moon and thought it was following you home. / Keep them up if you have ever noticed it is a *different shape* on different nights. / Right. Nearly all of you. / Today we are going to work out *why*.",
   "note": "Pause after each question so hands can go up, and land 'why' as the promise of the lesson."
  },
  {
   "id": "p2",
   "marked": "[cue: slide 2 - sunlight on the Moon] Here is the strange part. / The Moon does not make *any light* of its own. Not one bit. / Everything you have ever seen of the Moon is sunlight, ^ bouncing off grey rock and travelling [slow] about four hundred thousand kilometres to land in your eye.",
   "note": "A beat after the surprise, a breath before the long sentence, and slow on the big number."
  },
  {
   "id": "p3",
   "marked": "[cue: slide 3 - lit half and dark half] So at any moment, half of the Moon is lit by the Sun and half of it is in darkness, ^ exactly like the Earth has a day side and a night side. / That *never changes*. // [slow] What changes is how much of the lit half *we can see* from down here. //",
   "note": "This is the arrow: long pauses on both sides and slow delivery so it sticks."
  }
 ],
 "trims": [],
 "pacing": [],
 "open": "Look around the room, wait for quiet, take a two-second pause, then ask 'Hands up if you have ever looked at the Moon…' with your own hand raised.",
 "close": "End on the question 'what would that tell us about where it is?', said slowly after a short pause on 'But first'; then stay silent for a few seconds and do not answer it or add 'okay, so'.",
 "seconds_saved": 0
}

Shortened here: 2 more marks (p4, p5) left out. The page replays the whole reply.

remark: a eulogy after a rehearsal, re-marking the three paragraphs still rushed or dragged

Input (rehearsal holds the measured pace per paragraph; targets names the three to re-mark; p8 already carries a // mark):

{
 "task": "remark",
 "title": "For Grandma Rose",
 "occasion": "eulogy",
 "target_seconds": 210,
 "wpm": 125,
 "slides": false,
 "planned_seconds": 194,
 "paragraphs": "(a JSON string - decoded below)",
 "rehearsal": "(a JSON string - decoded below)",
 "targets": "p2,p5,p7",
 "clipped": ""
}

paragraphs, decoded (send it re-encoded as one string):

[
 {
  "id": "p1",
  "words": 25,
  "secs": 12.6,
  "full": true,
  "text": "Thank you all for being here. / Grandma Rose would have counted every one of you, and then she would have asked whether you had eaten."
 },
 {
  "id": "p2",
  "words": 72,
  "secs": 34.6,
  "full": true,
  "text": "She was born in 1931 in a two-room house in Cork, the youngest of seven, and she used to say she learned to talk early because otherwise nobody would have heard her. She came to Manchester at nineteen with one suitcase and the address of a cousin who, it turned out, had moved away the month before. She found work in a bakery that same week, and she stayed for thirty-one years."
 },
 {
  "id": "p3",
  "words": 42,
  "secs": 20.2,
  "full": true,
  "text": "Most of you knew her kitchen. The radio was always on, the kettle was always nearly boiling, and there was always a tin with something in it. If you ever told her you were not hungry, she heard that as a challenge."
 },
 {
  "id": "p4",
  "words": 89,
  "secs": 42.7,
  "full": true,
  "text": "But what I want to tell you about is a Tuesday in 1998. I was eleven, and I had been sent home from school for fighting. I expected her to be furious. Instead she sat me down, put a scone in front of me, and asked me what the other boy had said. When I told her, she was quiet for a long time. Then she said, you were right to be angry, and you were wrong to hit him, and both of those things are true at once."
 },
 {
  "id": "p5",
  "words": 56,
  "secs": 26.9,
  "full": true,
  "text": "I have thought about that sentence more than almost anything anyone has ever said to me. You can be right and wrong at once. You can be angry and still choose what to do with it. She lived that way. She was fierce about fairness and gentle with people, and she never once confused the two."
 },
 {
  "id": "p6",
  "words": 60,
  "secs": 28.8,
  "full": true,
  "text": "In the last few years her memory went, a little at a time. Some days she did not know my name. But she always knew whether I had eaten. And on one of her last good days she held my hand and told me I had turned out all right, which from Grandma Rose was the highest praise there is."
 },
 {
  "id": "p7",
  "words": 48,
  "secs": 23,
  "full": true,
  "text": "So here is what I am going to try to do, and what I would ask of you. When you are angry, be angry honestly. When you choose what to do next, choose kindly. And if someone tells you they are not hungry, put the kettle on anyway."
 },
 {
  "id": "p8",
  "words": 8,
  "secs": 5.4,
  "full": true,
  "text": "Thank you, Grandma. // We turned out all right."
 }
]

rehearsal, decoded (send it re-encoded as one string):

[
 {
  "id": "p1",
  "planned_secs": 12.6,
  "actual_secs": 12.6,
  "pace": "ok"
 },
 {
  "id": "p2",
  "planned_secs": 34.6,
  "actual_secs": 24.9,
  "pace": "rush"
 },
 {
  "id": "p3",
  "planned_secs": 20.2,
  "actual_secs": 20.2,
  "pace": "ok"
 },
 {
  "id": "p4",
  "planned_secs": 42.7,
  "actual_secs": 42.7,
  "pace": "ok"
 },
 {
  "id": "p5",
  "planned_secs": 26.9,
  "actual_secs": 19.9,
  "pace": "rush"
 },
 {
  "id": "p6",
  "planned_secs": 28.8,
  "actual_secs": 28.8,
  "pace": "ok"
 },
 {
  "id": "p7",
  "planned_secs": 23,
  "actual_secs": 30.4,
  "pace": "drag"
 },
 {
  "id": "p8",
  "planned_secs": 5.4,
  "actual_secs": 5.4,
  "pace": "ok"
 }
]

Reply:

{
 "task": "remark",
 "lane_note": "",
 "arrow": "You were right to be angry, and you were wrong to hit him, and both of those things are true at once.",
 "marks": [
  {
   "id": "p2",
   "marked": "She was born in 1931 / in a two-room house in Cork, ^ the youngest of seven, and she used to say she learned to talk early / because otherwise nobody would have heard her. / She came to Manchester at nineteen / with one suitcase and the address of a cousin who, it turned out, / had moved away the month before. / She found work in a bakery that same week, / and she stayed for [slow] *thirty-one years*.",
   "note": "This is her whole early life in three sentences; a beat after each one lets people picture it."
  },
  {
   "id": "p5",
   "marked": "I have thought about that sentence / more than almost anything anyone has ever said to me. // [slow] You can be *right and wrong* at once. / You can be angry / and still choose what to do with it. / She lived that way. / She was fierce about fairness / and gentle with people, ^ and she never once confused the two.",
   "note": "Her sentence is the heart of the talk, so give it silence before and say it slowly."
  },
  {
   "id": "p7",
   "marked": "So here is what I am going to try to do, and what I would ask of you. / When you are angry, be angry honestly. When you choose what to do next, choose kindly. / And if someone tells you they are not hungry, *put the kettle on* anyway.",
   "note": "Fewer stops so the three asks run as one line of thought and the kettle line lands warmly."
  }
 ],
 "trims": [],
 "pacing": [
  {
   "id": "p2",
   "issue": "rush",
   "advice": "Stop fully after \"nobody would have heard her\" and after \"the month before\", and say \"thirty-one years\" slowly before moving on."
  },
  {
   "id": "p5",
   "issue": "rush",
   "advice": "Take the long pause after \"ever said to me\", then say \"You can be right and wrong at once\" at half speed before going on."
  },
  {
   "id": "p7",
   "issue": "drag",
   "advice": "Say the two \"When you\" sentences together with no stop between them, and keep your one pause for just before \"And if someone tells you\"."
  }
 ],
 "open": "Reminder: look at the family, take one breath, then say \"Thank you all for being here.\"",
 "close": "Reminder: keep the long pause after \"Thank you, Grandma\", say \"We turned out all right\" slowly, and add nothing after it.",
 "seconds_saved": 0
}

The page checks the reply; the API does not

Over the API you get the reply as JSON and nothing has been applied or verified. The page's own code is one plain script you can load in node after global.window = {}: cuekit.js. CueKit.sectionize(text) splits a script into paragraphs p1..pn (a blank line between paragraphs, ## heading lines for sections); CueKit.buildInput(doc, {title, occasion, target, wpm, slides}, {task, targets, rehearsal}) returns {input, full, targets, ...} where input is this exact run body; CueKit.parseResult(text) and CueKit.normalize(obj, {task}) read the reply; CueKit.reconcile(result, doc, settings, {full, targets, rehearsal}) runs the checks above and returns per-mark and per-trim statuses, the recounted savedSecs, plannedAfter and a list of notes; CueKit.applyMarks(doc, recon) and CueKit.applyTrim(doc, id, cut) change the script; CueKit.toMarkdown(doc, settings) exports it.

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://cue-scroll.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":"cue-scroll"}' | 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": "mark", "title": "...", "occasion": "toast", "target_seconds": 135, "wpm": 140, "slides": false, "planned_seconds": 158, "paragraphs": "[...]", "rehearsal": "", "targets": "", "clipped": ""}
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 cue-scroll:<task>:<inputHash>:a<attempt>, 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="cue-scroll:mark:$(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: cue-scroll:mark:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json