← Read On Air
API

Drive Read On Air from your own code

The one metered task, edit, is available over HTTP. It takes a short personal belief essay (the kind read aloud on the radio in a few minutes), the time budget in minutes and the page's own counts of the draft (length against the budget at 150 words a minute, the sentences that state a belief, the sentences that preach, time markers and big abstract words). It returns a verdict (name_it, find_the_moment, off_the_pulpit or on_air), the belief in one sentence, the moment the draft tells, three to five notes that quote the draft word for word, a suggested first and last line, and a working title.

The division of labour first: the page owns the counts, the timing, the timed script and the verdict rule's two counted steps; the model owns judging whether the draft tells one particular moment and all the wording; the page then re-checks the reply against the draft.

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":"read-on-air"}), 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, minutes, draft, page_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: a producer's reading 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 and the verdicts

Every run sends task: edit (the one task is edit); a missing or different one is still answered as edit. The verdict follows a fixed check order, stopping at the first that fits: name_it when the page counts no sentence that states a belief in the first person; find_the_moment when the draft tells no one particular moment from the writer's life (the model's judgement); off_the_pulpit when the page counts 3 or more sentences that tell the listener what to do or think; otherwise on_air.

Input (every field is a string)

FieldRequiredWhat it holds
taskyesAlways edit.
minutesyesThe time budget: 2, 3 or 4 (300, 450 or 600 words at 150 words a minute).
draftyesThe essay, paragraphs separated by blank lines; over 9,000 characters it keeps its start and end with the middle marked as cut.
page_factsyesOne key | ... per line: length, sentences, belief_lines (count and each sentence quoted), pulpit_lines (count and each sentence quoted), time_markers, big_words, verdict_rule. Build it with CredoKit.buildInput (below) so the counts match the page's.
clip_notenoWhat the page shortened, if anything.
retry_notenoOnly on the page's one reformat retry, after an unusable reply.

Reply (one JSON object)

{"task": "edit", "title": "...", "belief": "I believe ...", "verdict": "on_air", "verdict_reason": "...",
 "moment": "...", "notes": [{"kind": "keep", "quote": "...", "text": "..."}, {"kind": "cut", "quote": "...", "text": "..."}],
 "first_line": "...", "last_line": "..."}
FieldWhat it holds
verdict, verdict_reasonThe verdict id by the check order above; one sentence of at most 30 words on what decided it.
beliefOne sentence starting "I believe", at most 30 words - the clearest belief sentence, or for name_it the belief the draft circles, in its own words.
momentThe moment the draft tells, 3 to 12 words copied from it; empty for find_the_moment.
notesThree to five {kind, quote, text}: kind is keep, cut, show or ask; quote is 3 to 25 words copied word for word from the draft; text at most 30 words.
first_line, last_lineSuggested opening and closing lines (at most 30 words each) built only from the draft's own details; first_line is empty for find_the_moment.
titleA working title of at most 8 words from the draft's own words.

The page then checks the reply (CredoKit.check): the verdict against its own counts, that the moment and every quote appear in the draft word for word, that a cut is named when the draft is over time, that the suggested lines use no names the draft does not, every number in the prose, leaked field ids and the word limits.

Worked examples

These are the four free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover all four verdicts - a tomato row that never says what it believes (name the belief), kindness told in big words (find the moment), a bus-stop good morning that turns into a lecture and runs over two minutes (step off the pulpit), and a porch light left on (ready for air).

A tomato row that never says what it believes - name the belief

Input:

{
 "task": "edit",
 "minutes": "2",
 "draft": "Every August my grandmother handed me a paper bag and pointed at the far row of tomatoes. She never came with me. She said the far row was mine because I was the only one patient enough to find the ripe ones under the leaves.\n\nThe summer I was nine, the bag tore. Eleven tomatoes rolled into the dirt and split. I cried so hard that the neighbour's dog started barking. My grandmother walked out, looked at the mess, and sat down in the dirt next to me. She picked up the worst one, wiped it on her apron and bit into it like an apple.\n\n\"Still a tomato,\" she said.\n\nWe ate four of them right there, sitting in the row, juice running down to our elbows. The rest went into a sauce that night. Nobody mentioned the bag.\n\nI am forty-one now and I still think about that row whenever something I care about splits open in front of me. A ruined thing is usually still the thing. You can sit down next to it.",
 "page_facts": "length | 177 words; about 1:11 read aloud at 150 words a minute (an estimate of a calm pace); the budget for 2 minutes is 300 words; 123 words under\nsentences | 15 sentences in 5 paragraphs; the longest has 23 words\nbelief_lines | 0 sentences state a belief in the first person\npulpit_lines | 0 sentences tell the listener what to do or think\ntime_markers | 3 found: \"august\", \"the summer i was nine\", \"that night\" (a hint only - markers are not a moment)\nbig_words | none of the page's list of big abstract words\nverdict_rule | check in order and stop at the first that fits: name_it when belief_lines counts 0 sentences; find_the_moment when the draft tells no one particular moment; off_the_pulpit when pulpit_lines counts 3 or more sentences; otherwise on_air"
}

Reply:

{
 "task": "edit",
 "title": "Still a Tomato",
 "belief": "I believe a ruined thing is usually still the thing, and you can sit down next to it.",
 "verdict": "name_it",
 "verdict_reason": "No sentence anywhere states the belief as I believe or similar, so the belief needs to be named before anything else.",
 "moment": "Eleven tomatoes rolled into the dirt and split",
 "notes": [
  {
   "kind": "keep",
   "quote": "She picked up the worst one, wiped it on her apron and bit into it like an apple.",
   "text": "This is the moment that earns the whole essay - plain, physical, and it shows exactly what the belief means without saying it."
  },
  {
   "kind": "ask",
   "quote": "A ruined thing is usually still the thing.",
   "text": "Is this the belief, stated plainly - or is there a shorter line still coiled inside it you haven't said yet?"
  },
  {
   "kind": "show",
   "quote": "I still think about that row whenever something I care about splits open in front of me.",
   "text": "This tells the feeling; the torn bag and the dirt already show it - trust that scene to carry the meaning here too."
  },
  {
   "kind": "keep",
   "quote": "You can sit down next to it.",
   "text": "This closing line lands because it comes straight out of the scene, not above it - keep it exactly as plain as this."
  }
 ],
 "first_line": "The summer I was nine, the bag tore and tomatoes rolled into the dirt.",
 "last_line": "A ruined thing is usually still the thing - you can sit down in the dirt next to it."
}

Kindness told in big words - find the moment

Input:

{
 "task": "edit",
 "minutes": "3",
 "draft": "I believe in kindness. Kindness is the most important value a person can have, and it is something the world needs more than ever.\n\nKindness costs nothing, but it means everything. It is the glue that holds communities together and the light that guides us through dark times. When people are kind, everything is better. When they are not, everything suffers.\n\nI have always tried to be a kind person. It is not always easy, because life can be hard and people can be difficult. But I think that kindness is a choice we make every day, and it is a choice that matters. Kindness is love in action.\n\nSome people think kindness is weakness. I disagree. Kindness takes courage and strength. It takes honesty and patience. It takes respect for the dignity of every human being.\n\nWe all need more of it. That is why I believe in kindness, and why I will keep believing in it for the rest of my life.",
 "page_facts": "length | 164 words; about 1:06 read aloud at 150 words a minute (an estimate of a calm pace); the budget for 3 minutes is 450 words; 286 words under\nsentences | 17 sentences in 5 paragraphs; the longest has 21 words\nbelief_lines | 2 sentences state a belief in the first person: [1] \"I believe in kindness.\" [2] \"That is why I believe in kindness, and why I will keep believing in it for the rest of my life.\"\npulpit_lines | 0 sentences tell the listener what to do or think\ntime_markers | none found (a hint only - a moment can be told without one)\nbig_words | kindness x8, courage x1, dignity x1, honesty x1, love x1, patience x1, respect x1, strength x1\nverdict_rule | check in order and stop at the first that fits: name_it when belief_lines counts 0 sentences; find_the_moment when the draft tells no one particular moment; off_the_pulpit when pulpit_lines counts 3 or more sentences; otherwise on_air"
}

Reply:

{
 "task": "edit",
 "title": "Kindness Is a Choice We Make",
 "belief": "I believe in kindness.",
 "verdict": "find_the_moment",
 "verdict_reason": "The draft offers a belief and general statements about kindness but never tells one specific scene from a particular time and place in the writer's life.",
 "moment": "",
 "notes": [
  {
   "kind": "ask",
   "quote": "I have always tried to be a kind person.",
   "text": "Picture one particular day you tried to be kind - where were you, and what happened in that moment?"
  },
  {
   "kind": "ask",
   "quote": "It is the glue that holds communities together and the light that guides us through dark times.",
   "text": "Was there one time you saw kindness hold a group together - who was there, and where did it happen?"
  },
  {
   "kind": "keep",
   "quote": "Kindness is love in action.",
   "text": "This line is plain, warm, and easy to say aloud - good to keep as is."
  }
 ],
 "first_line": "",
 "last_line": "That is why I believe in kindness, and why I will keep believing in it for the rest of my life."
}

A bus-stop good morning that lectures, over two minutes - step off the pulpit

Input:

{
 "task": "edit",
 "minutes": "2",
 "draft": "I believe in saying good morning to strangers.\n\nThe winter I was nineteen I worked the early shift at a bakery, and I caught the 5:40 bus from the corner by the laundromat. There were always the same six people at that stop, standing apart, hoods up, looking at their phones or at nothing. For three weeks none of us said a word.\n\nThen one morning an older man in a green scarf turned to me and said, \"Good morning.\" That was all. I was so surprised I said it back too loudly. The woman next to us laughed, and then she said it too.\n\nAfter that, the stop changed. We still mostly stood apart, but we said good morning. When the man in the green scarf stopped coming in February, three of us noticed on the same day and asked each other if anyone knew him. Nobody did. We had only ever known his good morning.\n\nYou should try it tomorrow. We must stop staring at our phones and start looking at each other again. Everyone needs to be seen by someone at least once a day, and it is our job to see them. Don't wait for someone else to go first. Society is falling apart because we have forgotten the simple things, and the simplest thing is two words. If we all did this, the world would be a better place, and loneliness would not be the problem it is today. Think about how many people you pass every day without a word. A smile is free. A greeting is free. Our grandparents knew this, and somewhere along the way we lost it. It really is that easy, and there is no excuse not to do it. Put the phone in your pocket, lift your head, and say it. It will change your day, and it might change theirs.\n\nI still say good morning at every bus stop, and most mornings somebody says it back.",
 "page_facts": "length | 328 words; about 2:11 read aloud at 150 words a minute (an estimate of a calm pace); the budget for 2 minutes is 300 words; 28 words over\nsentences | 27 sentences in 6 paragraphs; the longest has 27 words\nbelief_lines | 1 sentence states a belief in the first person: [1] \"I believe in saying good morning to strangers.\"\npulpit_lines | 4 sentences tell the listener what to do or think: [1] \"You should try it tomorrow.\" [2] \"We must stop staring at our phones and start looking at each other again.\" [3] \"Everyone needs to be seen by someone at least once a day, and it is our job to see them.\" [4] \"Don't wait for someone else to go first.\"\ntime_markers | 4 found: \"the winter i was nineteen\", \"5:40\", \"one morning\", \"february\" (a hint only - markers are not a moment)\nbig_words | none of the page's list of big abstract words\nverdict_rule | check in order and stop at the first that fits: name_it when belief_lines counts 0 sentences; find_the_moment when the draft tells no one particular moment; off_the_pulpit when pulpit_lines counts 3 or more sentences; otherwise on_air"
}

Reply:

{
 "task": "edit",
 "title": "Good Morning at the Bus Stop",
 "belief": "I believe in saying good morning to strangers.",
 "verdict": "off_the_pulpit",
 "verdict_reason": "Four lines near the end tell the listener what to do, enough to pull it off the air despite a clear belief and moment.",
 "moment": "an older man in a green scarf turned to me",
 "notes": [
  {
   "kind": "keep",
   "quote": "We had only ever known his good morning.",
   "text": "This last line of the bus-stop story lands quietly and lets the small moment carry the belief without any explaining."
  },
  {
   "kind": "cut",
   "quote": "We must stop staring at our phones and start looking at each other again.",
   "text": "This command preaches what the opening sentence already said; cutting it removes about fourteen words and shortens the piece toward time."
  },
  {
   "kind": "cut",
   "quote": "Everyone needs to be seen by someone at least once a day, and it is our job to see them.",
   "text": "This explains the belief instead of trusting the bus stop to show it; cutting removes twenty words toward the time budget."
  },
  {
   "kind": "show",
   "quote": "Society is falling apart because we have forgotten the simple things",
   "text": "This claim about society stays told; the bus stop scene already shows it, so trust that image instead of this line."
  }
 ],
 "first_line": "The winter I was nineteen, I caught the 5:40 bus from the corner by the laundromat, where six people always stood apart, hoods up.",
 "last_line": "I still believe in saying good morning to strangers, at every bus stop, the way the man in the green scarf once said it to me."
}

A porch light left on - ready for air

Input:

{
 "task": "edit",
 "minutes": "3",
 "draft": "I believe in leaving the porch light on.\n\nThe night I turned seventeen I missed the last train home. I had argued with my mother that afternoon about something I can no longer remember, and I had left the house saying I might not come back at all. By the time I walked the four miles from the station it was after two, and I was rehearsing what I would say when I found the door locked and the house dark.\n\nFrom the end of our street I could see it: the yellow bulb over the front step, the one with the dead moths in it, burning. The door was unlocked. My mother was asleep on the couch with her shoes still on. She had not waited up to shout at me. She had just left the light on and fallen asleep near the door.\n\nI did not wake her. I turned the light off, put a blanket over her, and went to bed. In the morning neither of us said anything about the argument. She made toast. I ate it.\n\nI have lived in five places since then, and I have left the porch light on in every one of them, even when nobody was out. My partner jokes that the electric bill has a line for it. My son, who is twelve, asked me once who I was waiting for. I told him I was not waiting for anybody. I was just making sure that if somebody came, they would know the door was the kind that opens.\n\nA light like that does not fix the argument. It does not say who was right. It only says that the way back is still there, and that somebody thought about it before they fell asleep.\n\nThat is what I believe in: a small light, left on, for whoever is still walking home.",
 "page_facts": "length | 313 words; about 2:05 read aloud at 150 words a minute (an estimate of a calm pace); the budget for 3 minutes is 450 words; 137 words under\nsentences | 23 sentences in 7 paragraphs; the longest has 33 words\nbelief_lines | 2 sentences state a belief in the first person: [1] \"I believe in leaving the porch light on.\" [2] \"That is what I believe in: a small light, left on, for whoever is still walking home.\"\npulpit_lines | 0 sentences tell the listener what to do or think\ntime_markers | 2 found: \"the night i turned seventeen\", \"that afternoon\" (a hint only - markers are not a moment)\nbig_words | none of the page's list of big abstract words\nverdict_rule | check in order and stop at the first that fits: name_it when belief_lines counts 0 sentences; find_the_moment when the draft tells no one particular moment; off_the_pulpit when pulpit_lines counts 3 or more sentences; otherwise on_air"
}

Reply:

{
 "task": "edit",
 "title": "The Porch Light, Left On",
 "belief": "I believe in leaving the porch light on.",
 "verdict": "on_air",
 "verdict_reason": "The draft names a clear belief, tells one particular night with time and place, and never tells the listener what to do - it's ready for air.",
 "moment": "The night I turned seventeen I missed the last train home",
 "notes": [
  {
   "kind": "keep",
   "quote": "the yellow bulb over the front step, the one with the dead moths in it, burning",
   "text": "This small detail makes the porch light real and particular, not just an idea - keep it exactly as is."
  },
  {
   "kind": "cut",
   "quote": "even when nobody was out",
   "text": "This trailing clause repeats what's already clear - cut it so the sentence lands cleaner."
  },
  {
   "kind": "show",
   "quote": "It does not say who was right",
   "text": "This states the lesson directly; the toast and quiet morning already show it without saying so."
  },
  {
   "kind": "ask",
   "quote": "I told him I was not waiting for anybody",
   "text": "What would it cost to tell him the truer, longer answer, the one this essay just gave?"
  }
 ],
 "first_line": "The night I turned seventeen, I missed the last train home and walked the four miles from the station after two.",
 "last_line": "I believe in a small light, left on, for whoever is still walking home, the kind of door that opens."
}

Build the input yourself

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

const K = CredoKit;
const built = K.buildInput({ minutes: "3", draft: "I believe in leaving the porch light on.\n\nThe night I turned seventeen..." });
built.a;                                         // { total, secs, budget, over, belief: [...], pulpit: [...], times, abstract, ... } - the free counts
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 reading
// K.whereIs("From the end of our street I could see it", built.form.draft)  // where a quote falls: { word: 81, time: "0:32" } in the porch sample

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://read-on-air.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":"read-on-air"}' | 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": "edit", "minutes": "3", "draft": "I believe in ...", "page_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 lane and the input: a retry with the same key never bills twice. The page uses read-on-air:<task>:<inputHash>:a<attempt> (the one task is edit), 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="read-on-air:edit:$(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: read-on-air:edit:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json