Drive Picture Book Desk from your own code
Everything the web page does is available over HTTP: send the manuscript page by page, say which age band it is for, attach the reading-level facts you computed in your own process, and get the same reading teacher's judgement back — or ask for a revision and get the pages rewritten to the band. The natural use is a slush pile read overnight, or a pipeline that re-levels every draft in a series whenever an editor changes the target age.
One thing to be clear about before the first call: the model never computes the numbers. Words,
sentences and syllables per page, the revised Spache grade, Flesch-Kincaid grade, Flesch reading ease, New
Dale-Chall, Dolch sight-word coverage, the rhyme scheme and syllables per line of every page, refrains, page-turn
cues and the phonics spelling patterns are all computed by the caller and sent as facts. The model's
job is judgement over those facts and over the words on the page — whether the book fits the children it is
for, whether the rhyme and the rhythm hold up read aloud, which words a child would stumble on, what to change.
See computing the facts yourself; the engine the web page uses ships as three plain scripts
(/wordlists.js, /dict.js, /leveler.js) you can load in node.
And one thing to be clear about the grades themselves: a readability grade measures independent decoding — the level at which a child could read the text alone. A picture book is normally read aloud to a child two to four years younger than the grade implies, which is why the desk's age bands sit well below the grades they allow.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses the same envelope, so one helper covers the whole API:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest endpoint takes {"slug":"picture-book-desk"} in its body),
so no slug header is needed afterwards — no app-slug header exists on this API at all. Send your token
as Authorization: Bearer … on every call.
The run body is the input object: post {"task": "level", …} directly. Wrapping it as
{"input": {…}} returns 200 and quietly hides every field from the model, so never do that.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a declared required field (task, brief, manuscript, target, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: running is metered and needs a personal token. Sign in to run. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
1. Get a token
A guest token is free and enough for /me and /estimate. Running a lane is metered, so it needs a personal token — sign in on the token page and copy it from there. A guest token that calls /run gets 403.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest -H "Content-Type: application/json" -d '{"slug":"picture-book-desk"}'
import json, urllib.request
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/guest", data=json.dumps({"slug": "picture-book-desk"}).encode(), headers={"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req))["data"]["token"])
const r = await fetch("https://api.skillsafe.ai/v1/app-api/guest", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ slug: "picture-book-desk" }) });
const { data } = await r.json(); console.log(data.token);
res, err := http.Post("https://api.skillsafe.ai/v1/app-api/guest", "application/json", strings.NewReader(`{"slug":"picture-book-desk"}`))
if err != nil { log.Fatal(err) }
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest")).header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"picture-book-desk\"}")).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
res = Net::HTTP.post(uri, { slug: "picture-book-desk" }.to_json, "Content-Type" => "application/json")
puts JSON.parse(res.body)["data"]["token"]
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => '{"slug":"picture-book-desk"}']);
echo json_decode(curl_exec($ch), true)["data"]["token"];
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/guest", new StringContent("{\"slug\":\"picture-book-desk\"}", Encoding.UTF8, "application/json"));
Console.WriteLine(await res.Content.ReadAsStringAsync());2. A tiny client
One helper, one envelope. The samples below reuse it.
curl -s -X GET https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json"
import json, urllib.request
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None):
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r: return json.load(r)
print(call("GET", "/me"))
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body) {
const r = await fetch("https://api.skillsafe.ai/v1/app-api" + path, { method, headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined });
return r.json();
}
console.log(await call("GET", "/me"));
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody()).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri, "Authorization" => "Bearer #{token}", "Content-Type" => "application/json")
puts Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }.body
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/me");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"]]);
echo curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Get, "https://api.skillsafe.ai/v1/app-api/me");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
Console.WriteLine(await (await http.SendAsync(req)).Content.ReadAsStringAsync());3. Check the session and the balance
GET /me returns subject_type (user or guest), subject_id and credits — and nothing else, so a signed-in caller is exactly subject_type === "user". A real user's first run should not 402: compare credits with the estimate's hold_credits before running.
curl -s -X GET https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json"
import json, urllib.request
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None):
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r: return json.load(r)
print(call("GET", "/me"))
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body) {
const r = await fetch("https://api.skillsafe.ai/v1/app-api" + path, { method, headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined });
return r.json();
}
console.log(await call("GET", "/me"));
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody()).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri, "Authorization" => "Bearer #{token}", "Content-Type" => "application/json")
puts Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }.body
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/me");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"]]);
echo curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Get, "https://api.skillsafe.ai/v1/app-api/me");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
Console.WriteLine(await (await http.SendAsync(req)).Content.ReadAsStringAsync());4. Price the run — free
POST /estimate with the exact run body returns model (gpt-5.6-terra), model_alias (gpt-terra), markup_bps (1000), hold_credits and min_credits. hold_credits is a reservation placed against the balance while the job runs, not the price: the unused part is refunded and the real cost comes back as charged_credits on the finished job. No job is created and nothing is charged by estimating. The body must be a JSON object with scalar string fields only — no nested objects, which is why facts travels as a JSON string.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None):
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r: return json.load(r)
print(call("POST", "/estimate", {"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}))
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body) {
const r = await fetch("https://api.skillsafe.ai/v1/app-api" + path, { method, headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined });
return r.json();
}
console.log(await call("POST", "/estimate", {"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}));
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/estimate", strings.NewReader(`{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}`))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}
""")).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/estimate")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{token}", "Content-Type" => "application/json")
req.body = "{\"task\": \"level\", \"brief\": \"A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.\", \"manuscript\": \"Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.\", \"target\": \"3-5\", \"facts\": \"<JSON string from Leveler.analyze(manuscript, {target}) - see below>\"}"
puts Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }.body
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/estimate");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}']);
echo curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/estimate");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
req.Content = new StringContent(@"{""task"": ""level"", ""brief"": ""A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud."", ""manuscript"": ""Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love."", ""target"": ""3-5"", ""facts"": ""<JSON string from Leveler.analyze(manuscript, {target}) - see below>""}", Encoding.UTF8, "application/json");
Console.WriteLine(await (await http.SendAsync(req)).Content.ReadAsStringAsync());The fields both lanes take
| Field | Type | Meaning |
|---|---|---|
task | string, required | The lane, and the first field to decide. level judges the manuscript you already have: whether it fits the children it is for, the read-aloud craft, the words a child would stumble on, the phonics it happens to teach. revise rewrites the pages to the band in target, keeping the story, the characters, the voice and — where the pages rhyme — the rhyme. Any other value is rejected as unknown; the reply always names the lane it answered in lane, and the two contracts are never blended. |
brief | string, required | Who the book is for and what you want checked or changed, in your own words. In the revise lane this is where the level lane's flagged words and your own constraints go (“keep the refrain”, “keep the ABAB rhyme”, “the mole is called Small Mole”). |
manuscript | string, required | The text of the book, pages separated by blank lines, dashes or Page N markers. Line breaks inside a page are kept and are what the rhyme scheme and the syllables-per-line are measured on. A long manuscript may be clipped in the middle with a marker; the facts are always computed from the whole text, never from the clipped copy. |
target | string, required | The age band: 2-3 (board book), 3-5 (picture book), 5-7 (early reader) or 7-9 (early chapter). It selects the desk's word-count and grade ceilings that the flags are raised against, and it is the band the revise lane writes to. |
facts | string, required | A JSON string (not an object), produced by Leveler.analyze(manuscript, {target}). Everything the model is allowed to quote as a number comes from here. See computing the facts yourself. |
retry_note | string | Optional, and normally absent. The page sends it only on the automatic reformat retry, when the first reply did not parse as one JSON object. |
Every field is a scalar string. Anything not in this table is rejected as unknown, and a missing task, brief, manuscript, target or facts is a 400 validation_error.
The desk's age bands
The bands are the desk's own trade guidance, not a rule of the trade, and the flags in facts.flags are raised against them. facts.summary.target_label names the band the manuscript was measured against.
target | Band | Words | Revised Spache grade | Dolch sight words |
|---|---|---|---|---|
2-3 | Board book | 0–150 | 1.9 or under | — |
3-5 | Picture book | 100–700 | 2.9 or under | — |
5-7 | Early reader | 200–1200 | 3.5 or under | 50% or more |
7-9 | Early chapter | 800–6000 | 4.5 or under | — |
Read those grades with the caveat at the top of this page in mind: a Spache grade of 2.9 is the level at which a child could decode the text alone, and the book is being read aloud to a three-year-old. The grade ceiling is a ceiling on the sentences and the vocabulary, not a claim about who the book is for.
Computing the facts yourself
Load the three engine scripts in node with a stub window, in this order, and call the same function the page calls. Everything the model is allowed to quote comes out of this one call:
global.window = {};
const vm = require("vm"), fs = require("fs");
for (const f of ["wordlists.js", "dict.js", "leveler.js"]) vm.runInThisContext(fs.readFileSync(f, "utf8"));
const Leveler = window.Leveler;
const manuscript = [
"Page 1",
"The sun slid down behind the hill.",
"Small Mole came home to bed.",
"The sky went pink. The wind went still.",
"She put a cap upon her head.",
"",
"Page 2",
"Her mother sang a sleepy song.",
"The moon came up so round.",
"The night was soft. The day was long.",
"There was no other sound.",
"",
"Page 3",
"Small Mole shut both her sleepy eyes.",
"The stars kept watch above.",
"Sleep well, small Mole, the night wind sighs.",
"Sleep well, my little love."
].join("\n");
const facts = Leveler.analyze(manuscript, { target: "3-5" }); // target: 2-3 | 3-5 | 5-7 | 7-9
// facts.summary pages, words, unique_words, ttr, sentences, syllables,
// asl (words per sentence), asw (syllables per word),
// words_per_page_mean / _max / _min, longest_sentence_words,
// longest_word, longest_word_syllables,
// spache_grade, spache_pdw, spache_unfamiliar, reading_age,
// fk_grade, flesch_ease,
// dale_chall, dale_chall_pdw, dale_chall_unfamiliar, dale_chall_band,
// dolch_pct, dolch_tokens, dolch_noun_pct, dolch_by_level,
// non_dolch_types, multisyllable_types, multisyllable_pct,
// rhymed_pages, rhyme_pct, meter_cv_mean, dialogue_lines,
// question_pages, cue_pages, refrains, target, target_label
// facts.pages[] page, lines, words, sentences, syllables, longest_sentence_words,
// line_syllables[], rhyme_scheme (AABB; ? for a last word the
// dictionary does not know), rhymed_lines, rhymes, dialogue_lines,
// ends_with_question, ends_with_cue, first_line
// facts.phonics keyed by cvc, digraph, blend, silent_e, vowel_team, r_controlled,
// each {label, types, examples[]}
// facts.spache_unfamiliar_words / dale_chall_unfamiliar_words / multisyllable_words /
// non_dolch_examples word lists, up to 40 each
// facts.refrain_lines[] {line, pages[]}
// facts.flags[] {id, text} with ids too_long, too_short, level_high, long_sentences,
// low_sight_words, hard_words, rhyme_uneven, meter_uneven,
// many_pages, few_pages
// facts.warnings[] engine warnings the model must address in notes_on_input
// facts.errors[] what could not be read at all
// facts.parse {pages, mode}
JSON.stringify(facts) // send this string as the facts field
Send JSON.stringify(facts) as facts — a string, not an object. The grades are the
published formulas: the revised Spache (1974) for the primary grade, Flesch-Kincaid (Kincaid et al. 1975) and
Flesch reading ease (1948) alongside it, and New Dale-Chall (Chall & Dale 1995) with its band. Sight-word
coverage is the Dolch list (1936). facts.summary.reading_age is the Spache grade plus five —
the age at which a child typically reads that grade alone, which is not the age the book is for.
Syllables and rhymes come from a pronunciation dictionary, so a last word the dictionary does not know makes that
line a ? in the page's rhyme_scheme rather than a guess.
The level request body, in full
Every field app.js sends, with facts abbreviated:
{
"task": "level",
"brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.",
"manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.",
"target": "3-5",
"facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"
}
Worked example: the level lane
Three pages of a rhyming bedtime book about a small mole, aimed at ages 3–5, and the question “does this fit?”. Price it first; the same body goes to /run.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None):
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r: return json.load(r)
print(call("POST", "/estimate", {"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}))
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body) {
const r = await fetch("https://api.skillsafe.ai/v1/app-api" + path, { method, headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined });
return r.json();
}
console.log(await call("POST", "/estimate", {"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}));
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/estimate", strings.NewReader(`{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}`))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}
""")).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/estimate")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{token}", "Content-Type" => "application/json")
req.body = "{\"task\": \"level\", \"brief\": \"A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.\", \"manuscript\": \"Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.\", \"target\": \"3-5\", \"facts\": \"<JSON string from Leveler.analyze(manuscript, {target}) - see below>\"}"
puts Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }.body
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/estimate");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}']);
echo curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/estimate");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
req.Content = new StringContent(@"{""task"": ""level"", ""brief"": ""A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud."", ""manuscript"": ""Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love."", ""target"": ""3-5"", ""facts"": ""<JSON string from Leveler.analyze(manuscript, {target}) - see below>""}", Encoding.UTF8, "application/json");
Console.WriteLine(await (await http.SendAsync(req)).Content.ReadAsStringAsync());The revise request body, in full
The same manuscript and the same band, but the brief now says what to change and what to keep, and the level lane's flagged words travel in it:
{
"task": "revise",
"brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.",
"manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.",
"target": "3-5",
"facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"
}
Worked example: the revise lane
Ask for the same three pages brought to the band with the rhyme kept. The reply is the whole book back, page by page — an unchanged page is repeated verbatim so you always hold a complete manuscript — plus what changed and what was deliberately kept. The model never states a word count or a grade for its own revision: re-run Leveler.analyze on the returned pages and compute the before and after yourself.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None):
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r: return json.load(r)
print(call("POST", "/estimate", {"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}))
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body) {
const r = await fetch("https://api.skillsafe.ai/v1/app-api" + path, { method, headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined });
return r.json();
}
console.log(await call("POST", "/estimate", {"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}));
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/estimate", strings.NewReader(`{"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}`))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}
""")).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/estimate")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{token}", "Content-Type" => "application/json")
req.body = "{\"task\": \"revise\", \"brief\": \"Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.\", \"manuscript\": \"Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.\", \"target\": \"3-5\", \"facts\": \"<JSON string from Leveler.analyze(manuscript, {target}) - see below>\"}"
puts Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }.body
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/estimate");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}']);
echo curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/estimate");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
req.Content = new StringContent(@"{""task"": ""revise"", ""brief"": ""Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word."", ""manuscript"": ""Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love."", ""target"": ""3-5"", ""facts"": ""<JSON string from Leveler.analyze(manuscript, {target}) - see below>""}", Encoding.UTF8, "application/json");
Console.WriteLine(await (await http.SendAsync(req)).Content.ReadAsStringAsync());5. Run it, then poll
POST /run returns {job_id}; GET /jobs/{job_id} until status is succeeded or failed. Send an Idempotency-Key header derived from the manuscript, the lane, the target and the facts so a retry never double-bills. The reply's output.output is the JSON text described in the contract below; charged_credits is the actual cost, and the unused part of the hold is released.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: picture-book-desk-level-0001" \
-d '{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None):
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r: return json.load(r)
print(call("POST", "/run", {"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}))
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body) {
const r = await fetch("https://api.skillsafe.ai/v1/app-api" + path, { method, headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined });
return r.json();
}
console.log(await call("POST", "/run", {"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}));
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run", strings.NewReader(`{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}`))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}
""")).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/run")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{token}", "Content-Type" => "application/json")
req.body = "{\"task\": \"level\", \"brief\": \"A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.\", \"manuscript\": \"Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.\", \"target\": \"3-5\", \"facts\": \"<JSON string from Leveler.analyze(manuscript, {target}) - see below>\"}"
puts Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }.body
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"task": "level", "brief": "A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}']);
echo curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
req.Content = new StringContent(@"{""task"": ""level"", ""brief"": ""A bedtime book for my niece, who is four. I want to know whether it fits ages 3-5 and whether the rhyme holds up read aloud."", ""manuscript"": ""Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love."", ""target"": ""3-5"", ""facts"": ""<JSON string from Leveler.analyze(manuscript, {target}) - see below>""}", Encoding.UTF8, "application/json");
Console.WriteLine(await (await http.SendAsync(req)).Content.ReadAsStringAsync());curl -s -X GET https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json"
import json, urllib.request
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None):
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r: return json.load(r)
print(call("GET", "/jobs/JOB_ID"))
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body) {
const r = await fetch("https://api.skillsafe.ai/v1/app-api" + path, { method, headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined });
return r.json();
}
console.log(await call("GET", "/jobs/JOB_ID"));
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.method("GET", HttpRequest.BodyPublishers.noBody()).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID")
req = Net::HTTP::Get.new(uri, "Authorization" => "Bearer #{token}", "Content-Type" => "application/json")
puts Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }.body
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"]]);
echo curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Get, "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
Console.WriteLine(await (await http.SendAsync(req)).Content.ReadAsStringAsync());6. Or stream it
POST /run-stream is server-sent events: job, then tick heartbeats, then done with the full output. Browsers receive ticks rather than text deltas, so build progress on elapsed time and parse the output from done. The revise lane is the long one — it writes every page back — so it is the one worth streaming.
curl -N -s -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Accept: text/event-stream" \
-H "Content-Type: application/json" -d '{"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}'
# events: job (the job id), tick (heartbeat), done (the full output). Browsers receive ticks, not deltas.
import json, urllib.request
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None):
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api" + path, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r: return json.load(r)
print(call("POST", "/run-stream", {"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}))
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body) {
const r = await fetch("https://api.skillsafe.ai/v1/app-api" + path, { method, headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined });
return r.json();
}
console.log(await call("POST", "/run-stream", {"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}));
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run-stream", strings.NewReader(`{"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}`))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/run-stream"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}
""")).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"; require "json"
uri = URI("https://api.skillsafe.ai/v1/app-api/run-stream")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{token}", "Content-Type" => "application/json")
req.body = "{\"task\": \"revise\", \"brief\": \"Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.\", \"manuscript\": \"Page 1\\nThe sun slid down behind the hill.\\nSmall Mole came home to bed.\\nThe sky went pink. The wind went still.\\nShe put a cap upon her head.\\n\\nPage 2\\nHer mother sang a sleepy song.\\nThe moon came up so round.\\nThe night was soft. The day was long.\\nThere was no other sound.\\n\\nPage 3\\nSmall Mole shut both her sleepy eyes.\\nThe stars kept watch above.\\nSleep well, small Mole, the night wind sighs.\\nSleep well, my little love.\", \"target\": \"3-5\", \"facts\": \"<JSON string from Leveler.analyze(manuscript, {target}) - see below>\"}"
puts Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }.body
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run-stream");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"task": "revise", "brief": "Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word.", "manuscript": "Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love.", "target": "3-5", "facts": "<JSON string from Leveler.analyze(manuscript, {target}) - see below>"}']);
echo curl_exec($ch);
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
req.Content = new StringContent(@"{""task"": ""revise"", ""brief"": ""Bring it to ages 3-5 and keep the ABAB rhyme, the name Small Mole and the last line. The level lane flagged sighs as the hardest word."", ""manuscript"": ""Page 1\nThe sun slid down behind the hill.\nSmall Mole came home to bed.\nThe sky went pink. The wind went still.\nShe put a cap upon her head.\n\nPage 2\nHer mother sang a sleepy song.\nThe moon came up so round.\nThe night was soft. The day was long.\nThere was no other sound.\n\nPage 3\nSmall Mole shut both her sleepy eyes.\nThe stars kept watch above.\nSleep well, small Mole, the night wind sighs.\nSleep well, my little love."", ""target"": ""3-5"", ""facts"": ""<JSON string from Leveler.analyze(manuscript, {target}) - see below>""}", Encoding.UTF8, "application/json");
Console.WriteLine(await (await http.SendAsync(req)).Content.ReadAsStringAsync());The output contract
One JSON object, no prose around it and no code fences. Common keys on every reply, whichever lane answered:
| Key | Type | Meaning |
|---|---|---|
lane | level | revise | The lane actually answered. If task was missing or unrecognised the closer lane is chosen from the input and named here. |
title | string | Under eighty characters, naming the situation — the manuscript, its size, its grade and its band. |
headline | string | One sentence: the verdict and the one thing that decides it. |
verdict | string | One of the lane's enum below. |
summary | string | Three to five sentences, read instead of the rest. |
notes_on_input[] | string[] | Anything unreadable, contradictory or missing in the input, including every entry of facts.warnings. |
risks[] | string[] | What could go wrong from here, one per string, grounded in the manuscript. |
next_steps[] | string[] | Ordered and concrete; each one starts with a verb. |
| Lane | Verdict | Body |
|---|---|---|
level |
fits · too_hard · too_easy · too_long · mixed · insufficient_text |
reading (three to five short paragraphs, every number quoted from facts: what the manuscript is, the level, the sentences and words, the read-aloud craft, the brief answered) · fit {words: under|in|over, level: under|in|over|unclear, why} · page_notes[] {page, note} · craft {rhyme, rhythm, refrain, page_turns} · vocabulary[] {word, why, alternative} · phonics_focus[] {pattern: cvc|digraph|blend|silent_e|vowel_team|r_controlled|sight_words|rhyming_family, words[], note} · suggestions[] {change, why, page} |
revise |
light_touch · revised · restructured · declined |
rationale (three or four short paragraphs: where the manuscript sits, what changed and what was kept, the craft decisions, what to read aloud) · pages[] {page, text} · changes[] {page, what, why} · kept[] · read_aloud_note |
In the level lane every entry in facts.flags is addressed somewhere — in
fit.why, a page_notes row or a suggestions row — and a flag the model
disagrees with is answered with the line that justifies the disagreement, not ignored. page_notes
lists only pages that need something, so a manuscript that fits may carry one or two rows.
vocabulary holds at most eight words, hardest first, and a character name is never a vocabulary
problem. phonics_focus carries two to four rows for a 5-7 or 7-9 band and
one to two for the younger bands, drawing on facts.phonics first. Every word in a
vocabulary entry and in a phonics_focus row appears in manuscript
case-insensitively — that is checkable, and worth checking.
In the revise lane pages covers pages 1..N of the manuscript in order, so the array is
the whole book: an unchanged page is repeated verbatim, a merged page keeps the lower number and a
changes row says so. Character names are kept. A page that rhymes in the manuscript rhymes with the
same scheme in the revision unless a changes row says why not. Line breaks inside a page are
\n.
The model states no word count, grade, age or percentage for its own revision, and it should not: those are
computed from the text it just wrote. Run Leveler.analyze(revisedText, {target}) over the joined
pages yourself and you get the after to set against the before — which is exactly what the web
page shows. The ban covers only figures that follow from the new text; the original manuscript's facts and the
desk's bands are quoted freely.
Reconcile the rest. Every figure the model writes in either lane is meant to be a verbatim copy out of
facts, so read them back: pull the numbers out of reading, fit.why,
craft and each suggestions row, compare them with the engine, and surface the
disagreements rather than the prose. The web page does this on every run and shows the author what did not match.
A sensible pipeline also checks the two membership rules above and, in the revise lane, that
pages really does cover every page.
insufficient_text and declined are real verdicts, not failures. Fewer than twenty words,
a non-empty facts.errors, or material in the manuscript that is not suitable for the stated age band
all produce one of them: the numbers stay on the page, the unsuitable passage is not reproduced, and
headline says why.
Derived from two agent skills by jamesrochabrun: @jamesrochabrun/kids-book-writer for the age-appropriate language, rhyme and read-aloud craft that both lanes follow, and @jamesrochabrun/reading-teacher for the reading-level and phonics judgement in the level lane. Reading ease follows Flesch (1948); the grade level follows Kincaid et al. (1975); the primary grade is the revised Spache (1974); the New Dale-Chall score follows Chall and Dale (1995); sight-word coverage uses the Dolch list (1936). A readability grade measures independent decoding, and a picture book is normally read aloud to a child two to four years younger. Not affiliated with the skills' author.