← Tell Me When
API

Drive Tell Me When from your own code

The one metered task, talk, is available over HTTP. It takes written instructions you have to read out to someone doing the job on the other end of a phone call - already split into numbered written steps, each with its word count and what trips the ear - plus the page's numbers. It returns a talk-through: the verdict, one line for you before you ring, what the listener should have within reach, an opening, short parts that each pair one thing to say with one check-in question, where to break for a second call, a closing question and one tip.

The division of labour first: the page counts; the model words it. The page splits the text into written steps, times each one at the chosen pace (slow 110, steady 140 or brisk 170 words a minute), marks what trips the ear (pointing at a picture, numbers and codes, symbols, abbreviations, asides in brackets, long sentences, several actions at once), works out the verdict, the break for two calls, how many parts to write, what the tip is about and whether safety applies, and sends them as facts. The model never changes those. After the reply, the page lists anything that does not match. A reading aid, not professional advice.

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": "..." } }

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

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, job, steps, facts) is missing.
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 talk-through is metered and needs a personal sign-in.
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, the input and the rules

Every run sends task: talk (the one task is talk); a missing or different one is still answered as talk. All fields are strings.

FieldRequiredWhat it holds
taskyesAlways talk.
jobyeswhat the job is | listener: ... | pace: steady (140 words a minute). The job or the listener is not named when empty; a job is up to 70 characters, a listener up to 40; the pace is slow (110), steady (140) or brisk (170).
stepsyesOne written step a line, in order: N. the step's text | W words | trips the ear: ... - the kinds of trouble with the exact words that cause them, or none. Two to twenty steps; a step is up to 300 characters.
factsyesThe page's numbers and choices: written steps, reading time at the pace, the steps that point at a picture, the steps that trip the ear (X of N), the verdict, the break, how many parts to write (a minimum and a maximum), what the tip is about, the safety line and the listener. Build it with TalkKit.buildInput.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The verdict, checked in this order: send-a-picture when a step points at a picture, diagram or figure the listener cannot see; else two-calls when there are 13 written steps or more; else slow-it-down when a third or more of the steps trip the ear; else read-it-out.

The reply

One JSON object, no prose around it:

{"task": "talk", "verdict": "read-it-out", "before_you_call": "...", "have_ready": ["..."], "opening": "...", "parts": [{"steps": [1], "say": "...", "check": "...?"}], "pause_after": 0, "pause_line": "", "closing": "...?", "tip": "..."}
FieldWhat it holds
verdictsend-a-picture, two-calls, slow-it-down or read-it-out - always the page's.
before_you_callOne sentence for you, the caller, about what to do before ringing: the picture to send (by step number), the two calls and where the first ends, how many steps trip the ear (X of N), or that the steps read well aloud.
have_readyA list of 0 to 6 short noun phrases, in the steps' own words, for the listener to have within reach. Empty when the steps name nothing to have ready.
openingOne or two sentences to the listener: the job in the input's own words, how many parts (a numeral equal to the length of parts), the things to get first, the break for two calls, and - when the page flags safety - a request to stop and say if anything seems wrong.
partsBetween the page's minimum and maximum, in the order of the written steps, each {"steps": [int], "say": "...", "check": "...?"}. steps names the written steps it comes from; every step appears at least once. say is at most 30 words and two sentences, one action each, keeping every number as the step writes it, abbreviations written out and symbols said as words. check is one question of 14 words or fewer that the listener answers before you go on.
pause_afterFor two-calls: the number (from 1) of the last part that includes the break step. Otherwise 0.
pause_lineFor two-calls: one sentence that ends the first call and says you will ring back, without a day or time. Otherwise empty.
closingOne question asking the listener to describe how it looks now - never a claim that it worked.
tipOne sentence for you about what the page says the tip is about (numbers and codes, symbols, abbreviations, an aside, a long sentence, several actions at once, or nothing), naming that step by its number.

The page reads the reply with ReplyKit.parseReply and ReplyKit.normalize, then re-checks it against the written steps and lists, under "The page's check", a verdict that differs from the page's, a step left out or covered out of order, a part count outside the range, a number dropped or added, a part that points at a page, a check that is not a question, a wrong break and a missing safety line.

Worked examples - every verdict

These are the free samples on the page: the exact input the page sends for each and the talk-through that replays for free. Together they cover every verdict - send a picture first, two calls, slow it down and read it out.

Tying a bow tie - send-a-picture

Input:

{
 "task": "talk",
 "job": "Tying a bow tie | listener: my brother | pace: steady (140 words a minute)",
 "steps": "1. Hang the tie around your neck so the left end hangs about 3 cm lower than the right. | 18 words | trips the ear: abbreviations (cm)\n2. Cross the longer end over the shorter end, close to your neck. | 12 words | trips the ear: none\n3. Bring the longer end up through the loop at your neck and let it hang down the front. | 18 words | trips the ear: none\n4. Fold the shorter end into a bow shape, as shown in the diagram, and hold it across your collar. | 19 words | trips the ear: points at a picture (as shown)\n5. Drop the longer end down over the front of the bow. | 11 words | trips the ear: none\n6. Fold the longer end into a second bow shape and push it through the loop behind the front bow. | 19 words | trips the ear: none\n7. Pull both folded loops gently to tighten, then straighten the bow. | 11 words | trips the ear: none",
 "facts": "Written steps: 7.\nReading it as written: 108 words, about 46 s at 140 words a minute, not counting the doing.\nSteps that point at a picture the listener cannot see: step 4.\nSteps that trip the ear: 2 of 7 - steps 1 and 4.\nVerdict: send-a-picture (checked in order: a step points at a picture is send-a-picture; else 13 steps or more is two-calls; else a third or more of the steps tripping the ear is slow-it-down; else read-it-out)\nBreak: none - one call.\nParts: write between 7 and 14 parts.\nTip about: abbreviations at step 1 (cm).\nSafety: nothing flagged.\nListener: my brother"
}

Reply:

{
 "task": "talk",
 "verdict": "send-a-picture",
 "before_you_call": "Before you call, send your brother the picture for step 4, showing a bow shape.",
 "have_ready": [
  "tie"
 ],
 "opening": "We're tying a bow tie, in 11 parts. Can you get your tie?",
 "parts": [
  {
   "steps": [
    1
   ],
   "say": "Hang the tie around your neck so the left end hangs about 3 centimeters lower than the right.",
   "check": "Is the left end hanging about 3 centimeters lower than the right?"
  },
  {
   "steps": [
    2
   ],
   "say": "Cross the longer end over the shorter end, close to your neck.",
   "check": "Have you crossed the longer end over the shorter end?"
  },
  {
   "steps": [
    3
   ],
   "say": "Bring the longer end up through the loop at your neck.",
   "check": "Is the longer end through the loop at your neck?"
  },
  {
   "steps": [
    3
   ],
   "say": "Let it hang down the front.",
   "check": "Is it hanging down the front?"
  },
  {
   "steps": [
    4
   ],
   "say": "Fold the shorter end into a bow shape, like the picture I sent you.",
   "check": "Does it look like the picture I sent you?"
  },
  {
   "steps": [
    4
   ],
   "say": "Hold it across your collar.",
   "check": "Is it across your collar?"
  },
  {
   "steps": [
    5
   ],
   "say": "Drop the longer end down over the front of the bow.",
   "check": "Is the longer end hanging over the front of the bow?"
  },
  {
   "steps": [
    6
   ],
   "say": "Fold the longer end into a second bow shape.",
   "check": "Is it folded into a second bow shape?"
  },
  {
   "steps": [
    6
   ],
   "say": "Push it through the loop behind the front bow.",
   "check": "Is it through the loop behind the front bow?"
  },
  {
   "steps": [
    7
   ],
   "say": "Pull both folded loops gently to tighten.",
   "check": "Are both folded loops tight now?"
  },
  {
   "steps": [
    7
   ],
   "say": "Straighten the bow.",
   "check": "Is the bow straight now?"
  }
 ],
 "pause_after": 0,
 "pause_line": "",
 "closing": "What does it look like now?",
 "tip": "At step 1, say 'cm' as 'centimeters.'"
}

Setting up a board game - two-calls

Input:

{
 "task": "talk",
 "job": "Setting up the board game | listener: Sam | pace: brisk (170 words a minute)",
 "steps": "1. Unfold the board in the middle of the table. | 9 words | trips the ear: none\n2. Give each player a screen and 5 coins. | 8 words | trips the ear: none\n3. Shuffle the green cards and put them face down by the board. | 12 words | trips the ear: none\n4. Shuffle the orange cards and put them face down next to the green ones. | 14 words | trips the ear: none\n5. Deal 3 green cards to each player. | 7 words | trips the ear: none\n6. Put the black pawn on the start square. | 8 words | trips the ear: none\n7. Each player picks a colour and takes the 4 pawns of that colour. | 13 words | trips the ear: none\n8. Put one of your pawns on the score track at zero. | 11 words | trips the ear: none\n9. Stack the gold tokens next to the bag. | 8 words | trips the ear: none\n10. Put the 12 crystals in the bag and shake it. | 10 words | trips the ear: none\n11. Draw 2 crystals from the bag and put them on the harbour spaces. | 13 words | trips the ear: none\n12. Turn the timer over so the sand is at the bottom. | 11 words | trips the ear: none\n13. The youngest player takes the first-player marker. | 7 words | trips the ear: none\n14. Leave the rule book open at the page with the round summary. | 12 words | trips the ear: none",
 "facts": "Written steps: 14.\nReading it as written: 143 words, about 50 s at 170 words a minute, not counting the doing.\nSteps that point at a picture the listener cannot see: none.\nSteps that trip the ear: 0 of 14.\nVerdict: two-calls (checked in order: a step points at a picture is send-a-picture; else 13 steps or more is two-calls; else a third or more of the steps tripping the ear is slow-it-down; else read-it-out)\nBreak: the first call ends after step 7; the second call starts at step 8.\nParts: write between 14 and 24 parts.\nTip about: nothing on the page trips the ear - waiting for each answer before going on.\nSafety: nothing flagged.\nListener: Sam"
}

Reply:

{
 "task": "talk",
 "verdict": "two-calls",
 "before_you_call": "There are 14 written steps, so plan two calls, with the first call ending after step 7.",
 "have_ready": [
  "board",
  "screen",
  "coins",
  "green cards",
  "orange cards",
  "black pawn",
  "pawns",
  "gold tokens"
 ],
 "opening": "We're setting up the board game, so get the board, a screen, coins, green cards, orange cards, the black pawn, pawns and the gold tokens ready first. There are 19 parts, and we'll stop for a break after part 10.",
 "parts": [
  {
   "steps": [
    1
   ],
   "say": "Unfold the board in the middle of the table.",
   "check": "Is the board unfolded?"
  },
  {
   "steps": [
    2
   ],
   "say": "Give each player a screen and 5 coins.",
   "check": "Does each player have a screen and 5 coins?"
  },
  {
   "steps": [
    3
   ],
   "say": "Shuffle the green cards.",
   "check": "Are the green cards shuffled?"
  },
  {
   "steps": [
    3
   ],
   "say": "Put them face down by the board.",
   "check": "Are the cards face down by the board?"
  },
  {
   "steps": [
    4
   ],
   "say": "Shuffle the orange cards.",
   "check": "Are the orange cards shuffled?"
  },
  {
   "steps": [
    4
   ],
   "say": "Put them face down next to the green ones.",
   "check": "Are the orange cards face down next to the green ones?"
  },
  {
   "steps": [
    5
   ],
   "say": "Deal 3 green cards to each player.",
   "check": "Does each player have 3 green cards?"
  },
  {
   "steps": [
    6
   ],
   "say": "Put the black pawn on the start square.",
   "check": "Is the black pawn on the start square?"
  },
  {
   "steps": [
    7
   ],
   "say": "Each player picks a colour.",
   "check": "Has each player picked a colour?"
  },
  {
   "steps": [
    7
   ],
   "say": "Each player takes the 4 pawns of that colour.",
   "check": "Does each player have the 4 pawns of that colour?"
  },
  {
   "steps": [
    8
   ],
   "say": "Put one of your pawns on the score track at zero.",
   "check": "Is one of your pawns on the score track at zero?"
  },
  {
   "steps": [
    9
   ],
   "say": "Stack the gold tokens next to the bag.",
   "check": "Are the gold tokens stacked next to the bag?"
  },
  {
   "steps": [
    10
   ],
   "say": "Put the 12 crystals in the bag.",
   "check": "Are the 12 crystals in the bag?"
  },
  {
   "steps": [
    10
   ],
   "say": "Shake it.",
   "check": "Have you shaken the bag?"
  },
  {
   "steps": [
    11
   ],
   "say": "Draw 2 crystals from the bag.",
   "check": "Have you drawn 2 crystals from the bag?"
  },
  {
   "steps": [
    11
   ],
   "say": "Put them on the harbour spaces.",
   "check": "Are the crystals on the harbour spaces?"
  },
  {
   "steps": [
    12
   ],
   "say": "Turn the timer over so the sand is at the bottom.",
   "check": "Is the sand at the bottom?"
  },
  {
   "steps": [
    13
   ],
   "say": "The youngest player takes the first-player marker.",
   "check": "Does the youngest player have the first-player marker?"
  },
  {
   "steps": [
    14
   ],
   "say": "Leave the rule book open at the page with the round summary.",
   "check": "Is the rule book open at the page with the round summary?"
  }
 ],
 "pause_after": 10,
 "pause_line": "I'll stop here and call you back to finish.",
 "closing": "Can you describe what the table looks like now?",
 "tip": "Nothing here is tricky to hear, so wait for Sam's answer after each part before you go on."
}

Pancake batter - slow-it-down

Input:

{
 "task": "talk",
 "job": "Making the pancake batter | listener: Dad | pace: slow (110 words a minute)",
 "steps": "1. Sift 125g plain flour + 1/4 tsp salt into a large bowl (the big glass one). | 16 words | trips the ear: numbers and codes (125g, 1/4); symbols (+, 1/4); abbreviations (tsp); an aside in brackets (the big glass one)\n2. Make a well in the middle & crack in 2 eggs. | 11 words | trips the ear: symbols (&)\n3. Pour in about 300ml milk a bit at a time, whisking all the while, then beat for approx. 2 mins until smooth; no lumps. | 24 words | trips the ear: numbers and codes (300ml); abbreviations (approx, mins); several actions at once (3 actions)\n4. Leave the batter to rest for 30 mins. | 8 words | trips the ear: abbreviations (mins)\n5. Heat 1 tsp oil in the frying pan on a medium heat. | 12 words | trips the ear: abbreviations (tsp)\n6. Pour in 1/2 a ladle of batter and tilt the pan so it covers the base. | 16 words | trips the ear: numbers and codes (1/2); symbols (1/2)\n7. Cook for 1 min, flip with the spatula & cook the other side for 30 secs. | 16 words | trips the ear: symbols (&); abbreviations (min, secs)",
 "facts": "Written steps: 7.\nReading it as written: 103 words, about 56 s at 110 words a minute, not counting the doing.\nSteps that point at a picture the listener cannot see: none.\nSteps that trip the ear: 7 of 7 - steps 1, 2, 3, 4, 5, 6 and 7.\nVerdict: slow-it-down (checked in order: a step points at a picture is send-a-picture; else 13 steps or more is two-calls; else a third or more of the steps tripping the ear is slow-it-down; else read-it-out)\nBreak: none - one call.\nParts: write between 7 and 14 parts.\nTip about: numbers and codes at step 1 (125g, 1/4).\nSafety: the steps mention \"heat\" and \"frying pan\" - keep every warning the steps give, and the opening asks them to stop and say if anything seems wrong.\nListener: Dad"
}

Reply:

{
 "task": "talk",
 "verdict": "slow-it-down",
 "before_you_call": "7 of 7 written steps are tricky to read aloud, so you will take them one small part at a time.",
 "have_ready": [
  "plain flour",
  "salt",
  "eggs",
  "milk",
  "oil",
  "frying pan",
  "ladle",
  "spatula"
 ],
 "opening": "We're making the pancake batter, in 12 parts, so can you get the plain flour, salt, eggs, milk, oil, frying pan, ladle and spatula ready first? Please stop and tell me if anything seems wrong.",
 "parts": [
  {
   "steps": [
    1
   ],
   "say": "Sift 125 grams of plain flour and 1/4 teaspoon of salt into a large bowl. It's the big glass one.",
   "check": "Have you sifted the flour and salt into the bowl?"
  },
  {
   "steps": [
    2
   ],
   "say": "Make a well in the middle.",
   "check": "Have you made a well in the middle?"
  },
  {
   "steps": [
    2
   ],
   "say": "Crack in 2 eggs.",
   "check": "Are the 2 eggs cracked in?"
  },
  {
   "steps": [
    3
   ],
   "say": "Pour in about 300 millilitres of milk a bit at a time. Whisk all the while.",
   "check": "Have you poured in the milk while whisking?"
  },
  {
   "steps": [
    3
   ],
   "say": "Beat for approximately 2 minutes until smooth, with no lumps.",
   "check": "Is it smooth with no lumps?"
  },
  {
   "steps": [
    4
   ],
   "say": "Leave the batter to rest for 30 minutes.",
   "check": "Have you set the batter to rest?"
  },
  {
   "steps": [
    5
   ],
   "say": "Heat 1 teaspoon of oil in the frying pan on a medium heat.",
   "check": "Has the oil heated in the pan?"
  },
  {
   "steps": [
    6
   ],
   "say": "Pour in 1/2 a ladle of batter.",
   "check": "Have you poured in the ladle of batter?"
  },
  {
   "steps": [
    6
   ],
   "say": "Tilt the pan so it covers the base.",
   "check": "Does it cover the base?"
  },
  {
   "steps": [
    7
   ],
   "say": "Cook for 1 minute.",
   "check": "Is it cooking?"
  },
  {
   "steps": [
    7
   ],
   "say": "Flip it with the spatula.",
   "check": "Have you flipped it?"
  },
  {
   "steps": [
    7
   ],
   "say": "Cook the other side for 30 seconds.",
   "check": "Is the other side cooking?"
  }
 ],
 "pause_after": 0,
 "pause_line": "",
 "closing": "How does it look now?",
 "tip": "For step 1, say the numbers slowly, then say them again, and let Dad repeat them back."
}

A new vacuum bag - read-it-out

Input:

{
 "task": "talk",
 "job": "Changing the vacuum cleaner bag | listener: Grandad | pace: steady (140 words a minute)",
 "steps": "1. Unplug the vacuum cleaner. | 4 words | trips the ear: none\n2. Press the grey button on top to open the front cover. | 11 words | trips the ear: none\n3. Lift out the full bag by its cardboard tab. | 9 words | trips the ear: none\n4. Drop the old bag straight into the bin. | 8 words | trips the ear: none\n5. Slide the new bag's cardboard tab into the holder until it clicks. | 12 words | trips the ear: none\n6. Close the cover and press it until it snaps shut. | 10 words | trips the ear: none",
 "facts": "Written steps: 6.\nReading it as written: 54 words, about 23 s at 140 words a minute, not counting the doing.\nSteps that point at a picture the listener cannot see: none.\nSteps that trip the ear: 0 of 6.\nVerdict: read-it-out (checked in order: a step points at a picture is send-a-picture; else 13 steps or more is two-calls; else a third or more of the steps tripping the ear is slow-it-down; else read-it-out)\nBreak: none - one call.\nParts: write between 6 and 12 parts.\nTip about: nothing on the page trips the ear - waiting for each answer before going on.\nSafety: nothing flagged.\nListener: Grandad"
}

Reply:

{
 "task": "talk",
 "verdict": "read-it-out",
 "before_you_call": "The steps read well aloud, so once you're on the phone with Grandad, the work is mostly waiting for each answer.",
 "have_ready": [
  "new bag"
 ],
 "opening": "We're changing the vacuum cleaner bag, and I'll walk you through it in 7 parts. Can you get the new bag ready first?",
 "parts": [
  {
   "steps": [
    1
   ],
   "say": "Unplug the vacuum cleaner.",
   "check": "Is the vacuum cleaner unplugged?"
  },
  {
   "steps": [
    2
   ],
   "say": "Press the grey button on top to open the front cover.",
   "check": "Is the front cover open?"
  },
  {
   "steps": [
    3
   ],
   "say": "Lift out the full bag by its cardboard tab.",
   "check": "Is the full bag out?"
  },
  {
   "steps": [
    4
   ],
   "say": "Drop the old bag straight into the bin.",
   "check": "Is the old bag in the bin?"
  },
  {
   "steps": [
    5
   ],
   "say": "Slide the new bag's cardboard tab into the holder until it clicks.",
   "check": "Did you hear it click?"
  },
  {
   "steps": [
    6
   ],
   "say": "Close the cover.",
   "check": "Is the cover closed?"
  },
  {
   "steps": [
    6
   ],
   "say": "Press it until it snaps shut.",
   "check": "Did it snap shut?"
  }
 ],
 "pause_after": 0,
 "pause_line": "",
 "closing": "What does the vacuum cleaner look like now?",
 "tip": "Just wait for Grandad to answer before you move on each time."
}

Build the input yourself

Load the engine in node (global.window = global; require("./talkkit.js"); require("./replykit.js")), then:

const K = TalkKit, R = ReplyKit;
const built = K.buildInput(K.form({ job: "Changing a light bulb", who: "Mum", pace: "steady",
  text: "Switch the lamp off at the wall.\nUnscrew the old bulb.\nScrew in the new bulb until it stops." }));  // built.ok is false, with built.note, until there are two steps
const wire = K.toWire(built.input);   // POST this to /estimate and /run; built.analysis holds rows, verdict, breakAfter, minParts, maxParts, tip, hazards
// then: const reply = R.normalize(R.parseReply(job.output.output));

Form keys: job (up to 70 characters), who (the listener, up to 40), pace (slow, steady or brisk) and text (the pasted instructions, one step a line, up to 4,000 characters). Leading markers such as 1., Step 2: or - are dropped; a single paragraph is split into sentences; only the first 20 steps are read.

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://tell-me-when.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":"tell-me-when"}' | 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": "talk", "job": "Changing the vacuum cleaner bag | listener: Grandad | pace: steady (140 words a minute)", "steps": "1. Unplug the vacuum cleaner. | 4 words | trips the ear: none ...", "facts": "...", ...}
# - see the field 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 task and the input: a retry with the same key never bills twice. The page uses tell-me-when:<task>:<inputHash>:a<attempt> (the one task is talk), 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="tell-me-when:talk:$(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: tell-me-when:talk:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json