← Under the Limit
API
Tokens

Drive Under the Limit from your own code

Both metered tasks - cutting an essay to its word limit, and reading it for voice - are available over HTTP. A natural use is a school counseling tool, a writing-center queue or a drafting app that already holds a student's essay: your code sends the paragraphs and the limit, and gets back cuts the student can accept or reject one by one, or the lines that sound like anyone's essay with a question for each.

The split first: the page counts; the model edits; the page checks it. The word count, the paragraphs P#, the filler cuts F#, the stock phrases G#, the checks X# and the suggested verdicts come from essaykit.js, which runs in node as well as in the browser. Its reconcileTrim then checks a cut reply: every find must be in the draft word for word, no replacement may bring in words the draft does not have, the verdict is never milder than the page's, and the count after the cuts is recounted. The model never writes new sentences: a cut only deletes or tightens the student's own words.

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":"under-the-limit"}), 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": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task 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: both tasks 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 comes first

Every run sends task. There are two tasks over the same draft:

taskWhat it returnsVerdict field
trimtitle, summary; cuts [{id, para, find, replace, kind, why}] with kind filler, tighten, redundant, drop-sentence or merge (replace is "" for a deletion); keeps [{para, quote, why}]; restructure [{para, action, why}] with action drop, merge, shorten or refocus; voice_notes[]; words_after.verdict: fits, tight or restructure
voicetitle, summary; lines [{id, para, quote, issue, why, ask}] with issue stock-phrase, brochure, formula, vague, telling, tidy-moral or signpost; strongest [{para, quote, why}]; cut_first (line ids that can simply go).verdict: yours, mixed or generic

fits means the cuts reach the limit with line edits alone; tight means at least one sentence of content had to go; restructure means a paragraph or an idea has to go and the reply says which. The page suggests a floor from the numbers: up to 15% over is fits, up to 40% tight, beyond that restructure; for voice, under 15% of sentences carrying a stock phrase is yours, under 50% mixed, else generic. A reply may be more severe than the floor, never milder.

The handoff between the tasks: send a voice reply's cut_first lines as flags_first on the next trim run and they are cut before anything else.

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (EssayKit.toWire does this). Only task is required by the schema.

Input fields

FieldTaskTypeNotes
taskbothstringtrim or voice.
kindbothstringpersonal-statement, supplement, short-answer or other.
limitbothnumberThe word limit, 25 to 2,000.
wordsbothnumberThe page's count of the whole draft.
over_bybothnumberwords - limit; negative when under.
paragraphsbothstring (JSON)[{id, words, text}], ids P1, P2... Past 12,000 characters whole paragraphs are left out of the middle and a CUT marker paragraph says which.
checksbothstring (JSON){suggested, why, fillers[{id, para, find, replace}], stock[{id, para, quote, kind}], notes[{id, kind, text}]} from EssayKit.state; suggested is for the task being run.
promptbothstringOptional, the essay question (600 characters).
keepbothstring (JSON)Optional [{id, text, para}]: lines the student will not cut.
notebothstringOptional, the student's own words about the essay (800 characters).
flags_firsttrimstring (JSON)Optional [{id, para, quote}] from a voice reply's cut_first.
retry_notebothstringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with EssayKit.parseReply, then normalizeTrim or normalizeVoice. applyCuts(state, reply.cuts, accepted) lays the cuts onto the draft (accepted maps a cut id to false to reject it) and returns the new text, its count and a status per cut; reconcileTrim(reply, state) and reconcileVoice(reply, state) return [{level, code, text}].

Worked examples

Replies produced against the app's prompt for the sample essays (made-up students and colleges; lists shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons. Inputs are shown with the long fields folded.

task: trim - Personal statement, 727 words, limit 650 (fits)

Input:

{
 "task": "trim",
 "kind": "personal-statement",
 "limit": 650,
 "words": 727,
 "over_by": 77,
 "paragraphs": "[{\"id\":\"P1\",\"words\":84,\"text\":\"Every Sunday at four, my grandmother's kitchen turns into a factory. The table is covered ...\"},\"... 6 more paragraphs\"]",
 "checks": "{\"suggested\":\"fits\",\"why\":\"77 over (12%) - usually reachable with line edits alone.\",\"fillers\":[{\"id\":\"F1\",\"para\":\"P1\",\"find\":\"honestly \",\"replace\":\"\"},\"...\"],\"stock\":[],\"notes\":[{\"id\":\"X1\",\"kind\":\"repeat\",\"text\":\"'a set screw that was two millimeters too short' appears in P4 and P6 - a deliberate echo, or a repeat to cut.\"},\"...\"]}",
 "prompt": "Write about a person, place or habit that shaped how you see the world."
}

Reply:

{
 "task": "trim",
 "verdict": "fits",
 "title": "Trimmed the hedges, kept the pin",
 "summary": "The draft ran 77 words over the 650-word limit (12% over), which the numbers alone call reachable with line edits. These cuts drop hedges and intensifiers ('I think that,' 'honestly,' 'very,' 'basically'), reword a few wordy setups ('I have to explain that I am a person who...', 'which is... who are...', 'which is... anyone is going to make a movie about'), and remove one redundant noticing example that only restates a pattern already shown. The dumpling-and-robotics echoes ('the bowl of filling a little closer,' 'a set screw that was two millimeters too short,' 'didn't say anything about it') and the whole bent-pin scene in paragraph 3 are untouched, so both moments the essay turns on still land. The cut draft sits 3 words under the limit.",
 "cuts": [
  {
   "id": "C1",
   "para": "P1",
   "find": "honestly ",
   "replace": "",
   "kind": "filler",
   "why": "intensifier adds nothing"
  },
  {
   "id": "C2",
   "para": "P1",
   "find": "very ",
   "replace": "",
   "kind": "filler",
   "why": "intensifier adds nothing"
  },
  "... 22 more"
 ],
 "keeps": [
  {
   "para": "P3",
   "quote": "So who will find your bent pin?",
   "why": "the line the whole essay is organized around - cutting it would cost the essay its spine"
  },
  {
   "para": "P4",
   "quote": "a set screw that was two millimeters too short",
   "why": "the specific detail that comes back in paragraph 6 and ties the two stories together"
  },
  "... 1 more"
 ],
 "restructure": [],
 "voice_notes": [
  "P5's 'not a story anyone is going to make a movie about' is a nice deflection, but it's the kind of joke a lot of essays reach for - is there a more specific way to say eleventh out of thirty-two felt?",
  "P7 opens with 'I want to study mechanical engineering because...' - a direct major statement many essays use to open the close - would starting from the noticing idea itself feel more like this draft?"
 ],
 "words_after": 647
}

task: trim - Why-us supplement, 309 words, limit 250 (tight)

Input:

{
 "task": "trim",
 "kind": "supplement",
 "limit": 250,
 "words": 309,
 "over_by": 59,
 "paragraphs": "[{\"id\":\"P1\",\"words\":91,\"text\":\"When I visited Halvorsen last spring, I expected the campus to feel like every other colle...\"},\"... 3 more paragraphs\"]",
 "checks": "{\"suggested\":\"tight\",\"why\":\"59 over (24%) - line edits alone rarely find that many; expect to lose a sentence or two.\",\"fillers\":[{\"id\":\"F1\",\"para\":\"P3\",\"find\":\"I believe that \",\"replace\":\"\"},\"...\"],\"stock\":[{\"id\":\"G1\",\"para\":\"P1\",\"quote\":\"vibrant community\",\"kind\":\"brochure\"},\"...\"],\"notes\":[{\"id\":\"X1\",\"kind\":\"opening\",\"text\":\"The first sentence carries a stock phrase (G1 'vibrant community') - the line a reader judges fastest.\"},\"...\"]}",
 "prompt": "Why do you want to attend Halvorsen College?",
 "keep": "[{\"id\":\"K1\",\"text\":\"when it finally turned on, he clapped\",\"para\":\"P1\"}]"
}

Reply:

{
 "task": "trim",
 "verdict": "tight",
 "title": "Brochure language cut, Priya's moment kept",
 "summary": "At 309 words this was 59 over the 250-word limit, about 24% over. Four cuts get it to 237 words, 13 under: a generic item dropped from a list in paragraph one, a stock-phrase opening sentence cut from paragraph two, a tacked-on closing line cut from paragraph three, and the whole closing paragraph, which was built entirely out of brochure phrases with no claim underneath them. Every anecdote with a name, a number or a place - Priya's lamp, the farmers market table, the inventory system - is untouched.",
 "cuts": [
  {
   "id": "C1",
   "para": "P1",
   "find": "beautiful buildings, a vibrant community, and a tour guide walking backwards",
   "replace": "beautiful buildings and a tour guide walking backwards",
   "kind": "redundant",
   "why": "Generic list item; the walking tour guide already carries this."
  },
  {
   "id": "C2",
   "para": "P2",
   "find": "Halvorsen is a place with world-class faculty and a strong commitment to diverse perspectives, and I know that I would grow there both academically and personally. ",
   "replace": "",
   "kind": "redundant",
   "why": "Pure brochure language; the specific anecdotes already show the fit."
  },
  "... 2 more"
 ],
 "keeps": [
  {
   "para": "P1",
   "quote": "when it finally turned on, he clapped",
   "why": "The specific, physical payoff of the Priya story - the essay's sharpest moment."
  },
  {
   "para": "P2",
   "quote": "I started a small repair table at our town's farmers market, and so far we have fixed eighty-one items, mostly lamps and bicycles.",
   "why": "A concrete number and place only this student has."
  },
  "... 1 more"
 ],
 "restructure": [],
 "voice_notes": [
  "The line about professors caring \"a lot\" tells instead of shows - ask what in their replies actually made that stick."
 ],
 "words_after": 237
}

task: trim - Activity essay, 250 words, limit 150 (restructure)

Input:

{
 "task": "trim",
 "kind": "short-answer",
 "limit": 150,
 "words": 250,
 "over_by": 100,
 "paragraphs": "[{\"id\":\"P1\",\"words\":59,\"text\":\"Ever since I was a young child, I have always had a deep passion for helping others and ma...\"},\"... 3 more paragraphs\"]",
 "checks": "{\"suggested\":\"restructure\",\"why\":\"100 over (67%) - more than line edits can take; a paragraph or an idea has to go.\",\"fillers\":[{\"id\":\"F1\",\"para\":\"P1\",\"find\":\"truly \",\"replace\":\"\"},\"...\"],\"stock\":[{\"id\":\"G1\",\"para\":\"P1\",\"quote\":\"Ever since I was a young child\",\"kind\":\"cliche\"},\"...\"],\"notes\":[{\"id\":\"X1\",\"kind\":\"opening\",\"text\":\"The first sentence carries a stock phrase (G1 'Ever since I was a young child') - the line a reader judges fastest.\"},\"...\"]}",
 "prompt": "Briefly describe one of your extracurricular activities.",
 "note": "I know it is way too long. The food drive is the part I actually care about."
}

Reply:

{
 "task": "trim",
 "verdict": "restructure",
 "title": "Cut the cliche, kept the food drive",
 "summary": "The draft came in 100 words over its 150-word limit (67% over). Cutting the stock openers and closers in paragraphs 1 and 4, the two lesson-sentences in paragraph 2, and a couple of cliche phrases in paragraph 3 brought it to 111 words - all line edits, no fact or event was dropped. The food-drive paragraph, the part the student said they care about, keeps its specifics: the 400 cans, the food pantry, stepping out of a comfort zone. The page's own count already called this a restructure, so that verdict stands here even though the cuts that got there were trims rather than a dropped idea.",
 "cuts": [
  {
   "id": "C1",
   "para": "P1",
   "find": "Ever since I was a young child, I have always had a deep passion for helping others and making a difference in my community. ",
   "replace": "",
   "kind": "redundant",
   "why": "Opening line is only stock phrases; nothing specific lost."
  },
  {
   "id": "C2",
   "para": "P1",
   "find": "I decided to join",
   "replace": "I joined",
   "kind": "tighten",
   "why": "Shorter phrasing, same meaning."
  },
  "... 8 more"
 ],
 "keeps": [
  {
   "para": "P3",
   "quote": "I helped organize a canned food drive that collected over 400 cans for a local food pantry.",
   "why": "The one countable, specific detail in the essay; it has to survive any further cut."
  },
  {
   "para": "P2",
   "quote": "food drives, park cleanups, tutoring younger students, and fundraising events",
   "why": "Shows what the student actually did, not just how they felt about it."
  }
 ],
 "restructure": [
  {
   "para": "P1",
   "action": "shorten",
   "why": "Keep only the fact of joining and when; the feelings around it are all stock phrases."
  },
  {
   "para": "P2",
   "action": "shorten",
   "why": "Keep the activity list only; the two lesson sentences just restate it."
  },
  "... 1 more"
 ],
 "voice_notes": [
  "P3: 'comfort zone' and 'a better leader' are common phrasing - what specifically was hard, and what did you do differently after?",
  "P2: 'important causes' doesn't say which one - naming a cause would ground the list."
 ],
 "words_after": 111
}

task: voice - Personal statement, 727 words, limit 650 (yours)

Input:

{
 "task": "voice",
 "kind": "personal-statement",
 "limit": 650,
 "words": 727,
 "over_by": 77,
 "paragraphs": "[{\"id\":\"P1\",\"words\":84,\"text\":\"Every Sunday at four, my grandmother's kitchen turns into a factory. The table is covered ...\"},\"... 6 more paragraphs\"]",
 "checks": "{\"suggested\":\"yours\",\"why\":\"No stock phrases found - the read looks for vaguer problems.\",\"fillers\":[{\"id\":\"F1\",\"para\":\"P1\",\"find\":\"honestly \",\"replace\":\"\"},\"...\"],\"stock\":[],\"notes\":[{\"id\":\"X1\",\"kind\":\"repeat\",\"text\":\"'a set screw that was two millimeters too short' appears in P4 and P6 - a deliberate echo, or a repeat to cut.\"},\"...\"]}",
 "prompt": "Write about a person, place or habit that shaped how you see the world."
}

Reply:

{
 "task": "voice",
 "verdict": "yours",
 "title": "Trust the dumplings over the moral",
 "summary": "The dumpling table, the bent-pin question, and the two-millimeter set screw are unmistakably this student's - specific, measured, and used twice on purpose. Two lines just name a trait or a feeling instead of showing it, and the last paragraph turns the whole essay into a stated lesson for life instead of ending on an image. Start by cutting the 'person who likes to be in charge of small things' line - the two examples right after it already make the point.",
 "lines": [
  {
   "id": "V1",
   "para": "P4",
   "quote": "I am basically a person who likes to be in charge of small things.",
   "issue": "telling",
   "why": "Names the trait directly; the next two sentences already show it without the label.",
   "ask": "Before robotics, what's one memory where you first noticed yourself needing to be in charge of something small?"
  },
  {
   "id": "V2",
   "para": "P5",
   "quote": "I am really proud of that",
   "issue": "telling",
   "why": "States the feeling; the freshmen detail right before it already proves the pride.",
   "ask": "What did one of those two better freshmen actually do that made you feel proud, not just know it?"
  },
  "... 2 more"
 ],
 "strongest": [
  {
   "para": "P3",
   "quote": "She said, \"So who will find your bent pin?\"",
   "why": "Her question turns a parable into real stakes - no one else's grandmother asks this exact thing."
  },
  {
   "para": "P4",
   "quote": "found a set screw that was two millimeters too short",
   "why": "A measured, specific number that only this robot and this night could have produced."
  },
  "... 1 more"
 ],
 "cut_first": [
  "V1",
  "V2",
  "V4"
 ]
}

task: voice - Why-us supplement, 309 words, limit 250 (mixed)

Input:

{
 "task": "voice",
 "kind": "supplement",
 "limit": 250,
 "words": 309,
 "over_by": 59,
 "paragraphs": "[{\"id\":\"P1\",\"words\":91,\"text\":\"When I visited Halvorsen last spring, I expected the campus to feel like every other colle...\"},\"... 3 more paragraphs\"]",
 "checks": "{\"suggested\":\"mixed\",\"why\":\"33% of sentences carry a stock phrase - specific lines sit next to generic ones.\",\"fillers\":[{\"id\":\"F1\",\"para\":\"P3\",\"find\":\"I believe that \",\"replace\":\"\"},\"...\"],\"stock\":[{\"id\":\"G1\",\"para\":\"P1\",\"quote\":\"vibrant community\",\"kind\":\"brochure\"},\"...\"],\"notes\":[{\"id\":\"X1\",\"kind\":\"opening\",\"text\":\"The first sentence carries a stock phrase (G1 'vibrant community') - the line a reader judges fastest.\"},\"...\"]}",
 "prompt": "Why do you want to attend Halvorsen College?",
 "keep": "[{\"id\":\"K1\",\"text\":\"when it finally turned on, he clapped\",\"para\":\"P1\"}]"
}

Reply:

{
 "task": "voice",
 "verdict": "mixed",
 "title": "Real scenes, borrowed brochure lines",
 "summary": "The Priya scene, the eighty-one-item repair table, and the inventory-system plan for the Thursday clinic are specific to this student and this visit. Around them sit sentences that are pure brochure sentiment - world-class faculty, collaborative culture, a perfect fit - that carry no fact of their own. Start by cutting the closing paragraph's claims and letting one of the specific scenes carry the ending instead.",
 "lines": [
  {
   "id": "V1",
   "para": "P2",
   "quote": "Halvorsen is a place with world-class faculty and a strong commitment to diverse perspectives, and I know that I would grow there both academically and personally.",
   "issue": "brochure",
   "why": "World-class, diverse perspectives, academically and personally - no specific professor, class, or moment here.",
   "ask": "Which specific class, professor, or moment made you feel you'd grow there?"
  },
  {
   "id": "V2",
   "para": "P2",
   "quote": "which told me a lot about how much they care about their students",
   "issue": "vague",
   "why": "Infers care from speed alone; nothing about what was said.",
   "ask": "What did one of those professors actually say in their reply that stuck with you?"
  },
  "... 3 more"
 ],
 "strongest": [
  {
   "para": "P1",
   "quote": "I watched a junior named Priya spend forty minutes on a lamp that belonged to a retired bus driver, and when it finally turned on, he clapped.",
   "why": "A named student, a specific object, and an earned reaction only this visit produced."
  },
  {
   "para": "P2",
   "quote": "At my high school, I started a small repair table at our town's farmers market, and so far we have fixed eighty-one items, mostly lamps and bicycles.",
   "why": "An exact count and a real project that is this student's own, not the college's."
  },
  "... 1 more"
 ],
 "cut_first": [
  "V1",
  "V2",
  "V3",
  "V4",
  "V5"
 ]
}

task: voice - Activity essay, 250 words, limit 150 (generic)

Input:

{
 "task": "voice",
 "kind": "short-answer",
 "limit": 150,
 "words": 250,
 "over_by": 100,
 "paragraphs": "[{\"id\":\"P1\",\"words\":59,\"text\":\"Ever since I was a young child, I have always had a deep passion for helping others and ma...\"},\"... 3 more paragraphs\"]",
 "checks": "{\"suggested\":\"generic\",\"why\":\"82% of sentences carry a stock phrase - most of the draft could be anyone's.\",\"fillers\":[{\"id\":\"F1\",\"para\":\"P1\",\"find\":\"truly \",\"replace\":\"\"},\"...\"],\"stock\":[{\"id\":\"G1\",\"para\":\"P1\",\"quote\":\"Ever since I was a young child\",\"kind\":\"cliche\"},\"...\"],\"notes\":[{\"id\":\"X1\",\"kind\":\"opening\",\"text\":\"The first sentence carries a stock phrase (G1 'Ever since I was a young child') - the line a reader judges fastest.\"},\"...\"]}",
 "prompt": "Briefly describe one of your extracurricular activities.",
 "note": "I know it is way too long. The food drive is the part I actually care about."
}

Reply:

{
 "task": "voice",
 "verdict": "generic",
 "title": "One true detail, buried in cliches",
 "summary": "The canned food drive - 400 cans, a real food pantry - is the only moment that is distinctly this student's; almost everything around it runs on borrowed phrasing. Three different paragraphs announce a lesson ('I have learned that', 'I realized that', 'It has taught me that') before the reader has seen enough to need telling, and the essay opens and closes on grand, generic claims about passion and transformation. Start by cutting the opening passion paragraph and the closing wrap-up, then build outward from the food drive with one more concrete beat.",
 "lines": [
  {
   "id": "V1",
   "para": "P1",
   "quote": "Ever since I was a young child, I have always had a deep passion for helping others and making a difference in my community.",
   "issue": "stock-phrase",
   "why": "Four stacked cliches about passion and helping before any specific detail appears.",
   "ask": "What's one specific moment - a day, a person, a task - that actually pulled you into this?"
  },
  {
   "id": "V2",
   "para": "P1",
   "quote": "it has been an incredible journey that has truly changed my life in so many ways",
   "issue": "stock-phrase",
   "why": "Two cliches claim transformation without saying what actually changed.",
   "ask": "What's one specific way you are different now than you were freshman year?"
  },
  "... 7 more"
 ],
 "strongest": [
  {
   "para": "P3",
   "quote": "organize a canned food drive that collected over 400 cans for a local food pantry",
   "why": "The one concrete number and place in the essay - this is the student's own story, not anyone's."
  }
 ],
 "cut_first": [
  "V1",
  "V2",
  "V3",
  "V4",
  "V6",
  "V7",
  "V8"
 ]
}

Build the input yourself

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

const K = EssayKit;
const st = K.state({
  kind: "supplement", limit: 250,
  prompt: "Why do you want to attend this college?",
  draft: "First paragraph...\n\nSecond paragraph...",
  keep: "a line you will not cut",   // optional, one per line
  note: ""                            // optional
});
// st.words, st.over, st.paras, st.fillers (F#), st.stock (G#), st.checks (X#), st.suggested.trim / .voice
const { input } = K.buildInput(st, "trim");          // or "voice"; trim takes { flagsFirst: [...] } as a third argument
const wire = K.toWire(input);                         // POST this to /estimate and /run
// later: const reply = K.normalizeTrim(K.parseReply(job.output.output));
// const applied = K.applyCuts(st, reply.cuts, {});    // applied.text, applied.words, applied.status
// K.reconcileTrim(reply, st) -> [{level, code, text}]

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://under-the-limit.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":"under-the-limit"}' | 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": "trim", "kind": "supplement", "limit": 250, "words": 309, "over_by": 59, "paragraphs": "[...]", "checks": "{...}"} or the same with "task": "voice"
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 under-the-limit:<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="under-the-limit:trim:$(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: under-the-limit:trim:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json