Run Essay Feedback from your own code
All three paid lanes are available over HTTP and each returns one JSON object: feedback (five criteria, a 1-4 band each, quotes from the essay and one revision step each), revision (before and after bands against that feedback) and transcribe (a photo of a handwritten page into text). No API key: a SkillSafe sign-in token, paid per run with credits. Feedback never contains a rewritten essay.
- Base URL and the response shape
- The input
- The replies
- Worked examples
- Steps 1-6: client, token, /me, /estimate, /run, /run-stream
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"essay-feedback"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare - never wrapped in {"input": ...}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a $files id is unknown. |
| 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: every lane is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job or file id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
task picks the lane. /estimate warns about unknown fields, so a wrapped or misspelled body shows up before you pay.
The input
| Field | Type | Lanes | What it is |
|---|---|---|---|
task | string | all | feedback, revision or transcribe. Required. |
essay | string | feedback, revision | The essay text with paragraph breaks (the first draft, for revision). The page sends at most 24,000 characters, cutting the middle with a marker that says how many words went. |
revised | string | revision | The revised draft. |
previous | string | revision | The earlier feedback, one entry per criterion: thesis: band 2 - suggestion: ... | structure: band 3 - .... |
title | string | feedback, revision | Optional. |
assignment | string | feedback, revision | The assignment prompt. Optional. |
essay_type | string | feedback, revision | One of: Argumentative, Persuasive, Expository / informative, Narrative, Literary analysis, Compare and contrast, Research paper, Personal statement / application, Reflective, Other. |
level | string | feedback, revision | One of: Middle school (grades 6-8), High school (grades 9-10), High school (grades 11-12), First-year college, Upper-level college, English language learner, Adult / professional. |
word_limit | string | feedback, revision | Like 300-500 words, at most 650 words, or empty. |
rubric | string | feedback, revision | The teacher's rubric, one line per criterion. Optional. |
teacher_notes | string | feedback, revision | Comments on an earlier draft. Optional. |
feedback_language | string | all | A language name, or the same language as the essay. |
facts | string | feedback, revision | The page's counts (words, sentences, readability, overused words, word-limit status; before -> after for revision). The model quotes these instead of counting. Compute your own or send an empty string. |
pages | number | transcribe | How many photos are attached. |
$files | array | transcribe | File ids from POST /files (multipart upload), in page order. Shrink photos to about 1600 px first. |
retry_note | string | any | Only when re-asking after an unreadable reply. |
The replies
Bands: 1 Beginning, 2 Developing, 3 Proficient, 4 Strong, judged for the stated level. Criteria ids, always in this order: thesis, structure, evidence, style, mechanics. Every quote is meant to be copied word for word from the essay; the page checks each one and flags any that is not.
feedback
{"task":"feedback","overview":"3-4 sentences: what the essay does, its biggest strength, the most important thing to work on",
"strengths":["2-3 specific things that work, each pointing at a place in the essay"],
"criteria":[
{"id":"thesis","band":1-4,"summary":"2-3 sentences on this criterion",
"quotes":[{"quote":"exact words from the essay","comment":"what this example shows (one sentence)"}],
"suggestion":"ONE concrete revision step for this criterion, under 60 words, not rewritten text"},
... one object for each of the five ids, in order ...],
"priorities":["the 2-3 revisions that will help most, in order, each one short sentence"],
"prompt_fit":"1-2 sentences on how fully the essay answers the assignment, or \"\" when assignment is empty",
"rubric_notes":[{"criterion":"the rubric line, shortened","met":"yes|partly|no","note":"one sentence"}],
"questions":["2-3 questions the student should ask themselves while revising"]}
revision
{"task":"revision","overview":"2-3 sentences: what got better, what did not",
"criteria":[
{"id":"thesis","before":1-4,"after":1-4,"change":"improved|same|weaker",
"acted_on":"yes|partly|no","evidence":[{"quote":"exact words from revised","comment":"one sentence"}],
"still":"what still needs work for this criterion, one sentence, or \"\" if nothing"},
... the five ids, in order ...],
"next_step":"the single most useful next revision, under 60 words, not rewritten text"}
transcribe
{"task":"transcribe","text":"the full transcription","legibility":"good|fair|poor",
"unclear":[{"near":"a few words around the spot","note":"what could not be read"}],
"note":"one sentence for the student (in feedback_language), e.g. how many pages were read"}
Worked examples
The page's own examples: the exact input it sends and a saved reply.
Grade 8 argument - feedback
Input:
{
"task": "feedback",
"title": "Should phones be banned in school?",
"assignment": "Should students be allowed to have phones in class? Write an argumentative essay that takes a clear position, supports it with reasons and evidence, and responds to at least one counterargument.",
"essay_type": "Argumentative",
"level": "Middle school (grades 6-8)",
"word_limit": "300-500 words",
"rubric": "Clear position stated in the introduction\nAt least two reasons, each supported with evidence or an example\nResponds to a counterargument\nParagraphs with topic sentences and transitions\nCorrect spelling and punctuation",
"teacher_notes": "",
"feedback_language": "the same language as the essay",
"essay": "Phones In School\n\nPhones are a big part of our lives today. Almost every student has one and they bring it to school everyday. In this essay I will talk about if phones should be banned in school. I think phones should be banned in class because they are distracting.\n\nFirst, phones are very distracting. When a phone buzzes everyone looks at it and then nobody is paying attention to the teacher anymore and the teacher has to stop and wait and then the lesson takes longer and we dont finish the work. Phones are really distracting for students. Even if you dont touch your phone you are thinking about it.\n\nSecond, phones can be used for cheating. Some students take pictures of the test and send it to there friends in other classes. This is not fair to the students who study. Teachers cant see everything so phones make it easy to cheat.\n\nSome people say that phones are good for emergencys. If something happens parents want to call there kids. But the school office has a phone and parents can call it. So this is not a good reason.\n\nAlso phones cause cyberbullying. Students post mean things about each other at lunch and in class. This makes alot of students feel bad and they dont want to come to school.\n\nIn conclusion phones should be banned in school because they are distracting, they can be used for cheating and they cause cyberbullying. Schools should make a rule that phones stay in lockers. That way students can focus on learning.",
"facts": "words 258 (title line not counted); sentences 22; paragraphs 6; average sentence 11.7 words; longest sentence 38 words; sentences of 35+ words 1; Flesch reading ease 83.4 (very easy), grade level 4.5; passive constructions about 5; overused words [phones x11, students x6, school x5, distracting x4]; repeated sentence openers [none]; filler phrases [very x1, really x1, things x1, i think x1]; word limit 300-500 words: UNDER"
}
Reply:
{
"task": "feedback",
"overview": "Your essay takes a clear stance against phones in class and organizes your argument into distinct paragraphs, including a solid response to the emergency counterargument - that's a real strength. At 258 words, though, the essay is under the 300-500 word limit, so the most important fix is adding content: specific evidence for your distraction and cyberbullying points, and a thesis that previews all three reasons instead of just one. Cleaning up recurring spelling slips (there/their, missing apostrophes) will also help.",
"strengths": [
"Your paragraph on cheating gives a specific, concrete example (students photographing tests and texting them to friends), which makes that reason convincing.",
"You directly address the counterargument about emergencies and answer it with the school office phone idea, which is exactly what the rubric asks for.",
"Your conclusion does more than repeat the reasons - it proposes a real policy (phones stay in lockers), giving the essay a sense of purpose."
],
"criteria": [
{
"id": "thesis",
"band": 3,
"summary": "You state a clear, arguable position by the end of the intro, which answers the assignment's prompt directly. However, the thesis names only one reason (distraction) even though your essay actually argues three, so it doesn't fully preview your case.",
"quotes": [
{
"quote": "I think phones should be banned in class because they are distracting.",
"comment": "This is a clear, arguable claim, but it only names one of your three reasons."
},
{
"quote": "In this essay I will talk about if phones should be banned in school.",
"comment": "This sentence announces the topic instead of taking a position, delaying your actual thesis."
}
],
"suggestion": "At the end of your intro, revise your thesis sentence to briefly name all three reasons (distraction, cheating, cyberbullying) instead of just one, so it previews the whole essay."
},
{
"id": "structure",
"band": 3,
"summary": "Each paragraph has one clear job and starts with a topic sentence, and you have a real intro and conclusion. The main gap is that the counterargument paragraph sits between your second and third reasons, which breaks up the sequence of your case, and transition words repeat.",
"quotes": [
{
"quote": "First, phones are very distracting.",
"comment": "A clear topic sentence that signals the paragraph's job."
},
{
"quote": "Some people say that phones are good for emergencys.",
"comment": "The counterargument appears before your third reason, interrupting the flow of your argument."
},
{
"quote": "Also phones cause cyberbullying.",
"comment": "A generic transition word that doesn't connect back to the ideas before it."
}
],
"suggestion": "Move the counterargument paragraph (about emergencies) to right before your conclusion, after all three reasons, so readers get your full case before you address objections."
},
{
"id": "evidence",
"band": 2,
"summary": "Your cheating paragraph has one genuinely specific example, but the distraction and cyberbullying paragraphs mostly state effects without a concrete instance. The counterargument response is reasoned but also lacks real evidence, like a fact or a specific situation.",
"quotes": [
{
"quote": "Some students take pictures of the test and send it to there friends in other classes.",
"comment": "A specific, concrete example that makes this reason convincing."
},
{
"quote": "Students post mean things about each other at lunch and in class.",
"comment": "This is a general claim, not a specific example of what happened."
},
{
"quote": "But the school office has a phone and parents can call it.",
"comment": "A reasonable rebuttal, but it offers no evidence that this actually works in practice."
}
],
"suggestion": "For the distraction and cyberbullying paragraphs, add one specific example each, like a real classroom moment or a described incident, the way you did with the test-photo example in the cheating paragraph."
},
{
"id": "style",
"band": 2,
"summary": "Sentences are clear and appropriate for your grade level, but word choice is repetitive (phones, students, distracting each appear many times) and filler words like 'really' and 'very' weaken your sentences. Sentence length is also very uniform.",
"quotes": [
{
"quote": "Phones are a big part of our lives today.",
"comment": "A generic opening line that could be more specific to your argument."
},
{
"quote": "Phones are really distracting for students.",
"comment": "The filler word 'really' and repeated nouns make this sentence feel padded rather than sharp."
}
],
"suggestion": "Replace at least two of the eleven uses of 'phones' with pronouns or synonyms like 'they' or 'devices', and cut filler words like 'really' and 'very' to sharpen your sentences."
},
{
"id": "mechanics",
"band": 2,
"summary": "Punctuation at the sentence level is mostly fine, but there are recurring spelling and apostrophe errors throughout: missing apostrophes in contractions, there/their mix-ups, and one misspelled word.",
"quotes": [
{
"quote": "Some people say that phones are good for emergencys.",
"comment": "'emergencys' should be spelled 'emergencies'."
},
{
"quote": "send it to there friends in other classes",
"comment": "'there' should be 'their' here."
},
{
"quote": "we dont finish the work",
"comment": "'dont' is missing its apostrophe (don't)."
}
],
"suggestion": "Proofread specifically for missing apostrophes in contractions (dont, cant) and for there/their mix-ups; reading the essay aloud slowly can help you catch these."
}
],
"priorities": [
"Expand the essay to meet the word limit (it's currently under at 258 words) by adding specific evidence to your weaker paragraphs.",
"Revise your thesis to preview all three reasons, not just distraction.",
"Fix the recurring apostrophe and spelling slips (dont, cant, there/their, emergencys)."
],
"prompt_fit": "Your essay takes a clear stance against phones in class and gives multiple reasons plus a counterargument, which directly answers the assignment; to fully meet it, add more specific evidence and reach the required word count.",
"rubric_notes": [
{
"criterion": "Clear position stated in the introduction",
"met": "partly",
"note": "The position is stated clearly at the end of the intro, but the thesis only mentions one of your three reasons."
},
{
"criterion": "At least two reasons, each supported with evidence or an example",
"met": "partly",
"note": "You give three reasons, but only the cheating paragraph has a specific, concrete example."
},
{
"criterion": "Responds to a counterargument",
"met": "yes",
"note": "You address the emergency argument directly and give a reasoned rebuttal about the school office phone."
},
{
"criterion": "Paragraphs with topic sentences and transitions",
"met": "partly",
"note": "Each paragraph has a topic sentence, but transitions are repetitive and the counterargument interrupts the sequence of reasons."
},
{
"criterion": "Correct spelling and punctuation",
"met": "partly",
"note": "Punctuation is mostly fine, but there are recurring errors like missing apostrophes and there/their mix-ups."
}
],
"questions": [
"Does your thesis sentence mention all three reasons you actually argue, or just one of them?",
"Where could you add one specific, concrete detail to the distraction and cyberbullying paragraphs, similar to the test-photo example?",
"Does placing the counterargument between your second and third reasons help or confuse your argument's flow?"
]
}
Grade 8 argument - revision
Input:
{
"task": "revision",
"title": "Should phones be banned in school?",
"assignment": "Should students be allowed to have phones in class? Write an argumentative essay that takes a clear position, supports it with reasons and evidence, and responds to at least one counterargument.",
"essay_type": "Argumentative",
"level": "Middle school (grades 6-8)",
"word_limit": "300-500 words",
"rubric": "Clear position stated in the introduction\nAt least two reasons, each supported with evidence or an example\nResponds to a counterargument\nParagraphs with topic sentences and transitions\nCorrect spelling and punctuation",
"teacher_notes": "",
"feedback_language": "the same language as the essay",
"essay": "Phones In School\n\nPhones are a big part of our lives today. Almost every student has one and they bring it to school everyday. In this essay I will talk about if phones should be banned in school. I think phones should be banned in class because they are distracting.\n\nFirst, phones are very distracting. When a phone buzzes everyone looks at it and then nobody is paying attention to the teacher anymore and the teacher has to stop and wait and then the lesson takes longer and we dont finish the work. Phones are really distracting for students. Even if you dont touch your phone you are thinking about it.\n\nSecond, phones can be used for cheating. Some students take pictures of the test and send it to there friends in other classes. This is not fair to the students who study. Teachers cant see everything so phones make it easy to cheat.\n\nSome people say that phones are good for emergencys. If something happens parents want to call there kids. But the school office has a phone and parents can call it. So this is not a good reason.\n\nAlso phones cause cyberbullying. Students post mean things about each other at lunch and in class. This makes alot of students feel bad and they dont want to come to school.\n\nIn conclusion phones should be banned in school because they are distracting, they can be used for cheating and they cause cyberbullying. Schools should make a rule that phones stay in lockers. That way students can focus on learning.",
"facts": "words 258 -> 314; paragraphs 6 -> 6; average sentence 11.7 -> 15.7 words; sentences of 35+ words 1 -> 0; new or reworded sentences 20 of 20 (100%); sentences cut 22; Flesch reading ease 83.4 -> 74; no longer overused [distracting]; newly overused [class, phone]; word limit 300-500 words: UNDER -> WITHIN",
"revised": "Phones In School\n\nAlmost every student at our school carries a phone, and most of us check it during class without thinking. Schools should require phones to stay in lockers during class time, because phones pull attention away from learning, make cheating easier, and give bullying a place to happen during the school day.\n\nThe biggest problem is attention. When a phone buzzes, the whole row looks at it, and the teacher has to stop and wait. In my science class last month, our teacher counted eleven interruptions from phones in one lesson. Even when a phone is silent, students think about it. Our class lost about ten minutes that day, so the lesson took longer and we did not finish the lab.\n\nPhones also make cheating easier. Some students photograph a test and send it to friends who take it later. This is unfair to students who studied, and teachers cannot watch every desk at once.\n\nSome people argue that students need phones for emergencies, because parents want to reach their children. That is a real concern. However, the school office has a phone, and during class an emergency call would go through the office anyway. Phones in lockers can still be used at lunch and after school.\n\nFinally, phones give cyberbullying a place to happen during the day. Students post mean comments about each other at lunch and even in class. Last year a group chat in my grade shared an embarrassing photo of a classmate during lunch, and by sixth period everyone had seen it. When that can happen in the middle of the day, the students who are targeted can feel unsafe at school, not just online.\n\nIn conclusion, keeping phones in lockers during class would help students focus, protect fair testing, and make school feel safer. A clear locker rule is a small change that would let class time be about learning.",
"previous": "thesis: band 3 - suggestion: At the end of your intro, revise your thesis sentence to briefly name all three reasons (distraction, cheating, cyberbullying) instead of just one, so it previews the whole essay. | structure: band 3 - suggestion: Move the counterargument paragraph (about emergencies) to right before your conclusion, after all three reasons, so readers get your full case before you address objections. | evidence: band 2 - suggestion: For the distraction and cyberbullying paragraphs, add one specific example each, like a real classroom moment or a described incident, the way you did with the test-photo example in the cheating paragraph. | style: band 2 - suggestion: Replace at least two of the eleven uses of 'phones' with pronouns or synonyms like 'they' or 'devices', and cut filler words like 'really' and 'very' to sharpen your sentences. | mechanics: band 2 - suggestion: Proofread specifically for missing apostrophes in contractions (dont, cant) and for there/their mix-ups; reading the essay aloud slowly can help you catch these."
}
Reply:
{
"task": "revision",
"overview": "This revision is a clear step up: the thesis now previews all three reasons, two of your three body paragraphs gained the kind of concrete, specific example you used for cheating in draft one, and the apostrophe/there-their errors are gone. The counterargument paragraph, however, is still sitting in the same spot as before, so that suggestion wasn't acted on, and 'phones' is still repeated instead of varied.",
"criteria": [
{
"id": "thesis",
"before": 3,
"after": 4,
"change": "improved",
"acted_on": "yes",
"evidence": [
{
"quote": "phones pull attention away from learning, make cheating easier, and give bullying a place to happen during the school day",
"comment": "The thesis now names all three reasons, previewing the whole essay as suggested."
}
],
"still": "Nothing major here now; the claim is specific and arguable and matches the essay's structure."
},
{
"id": "structure",
"before": 3,
"after": 3,
"change": "same",
"acted_on": "no",
"evidence": [
{
"quote": "Some people argue that students need phones for emergencies, because parents want to reach their children.",
"comment": "The counterargument paragraph is still placed before the cyberbullying reason, not moved after all three reasons as suggested."
}
],
"still": "Move the emergencies paragraph so it comes after the cyberbullying paragraph, right before the conclusion."
},
{
"id": "evidence",
"before": 2,
"after": 4,
"change": "improved",
"acted_on": "yes",
"evidence": [
{
"quote": "our teacher counted eleven interruptions from phones in one lesson",
"comment": "A concrete, numbered classroom example now supports the distraction paragraph."
},
{
"quote": "a group chat in my grade shared an embarrassing photo of a classmate during lunch, and by sixth period everyone had seen it",
"comment": "A specific incident now supports the cyberbullying paragraph, matching the earlier test-photo example."
}
],
"still": "The cheating paragraph still describes things in general terms; a brief specific instance there would match the detail level of the other two paragraphs."
},
{
"id": "style",
"before": 2,
"after": 3,
"change": "improved",
"acted_on": "partly",
"evidence": [
{
"quote": "The biggest problem is attention.",
"comment": "Filler words like 'really' and 'very' are gone, making sentences sharper."
},
{
"quote": "Phones in lockers can still be used at lunch and after school.",
"comment": "The word 'phones' is still used instead of a pronoun or synonym, as the earlier suggestion asked you to vary."
}
],
"still": "Swap a couple more instances of 'phones' for 'they' or 'devices' so the word doesn't repeat so often across paragraphs."
},
{
"id": "mechanics",
"before": 2,
"after": 4,
"change": "improved",
"acted_on": "yes",
"evidence": [
{
"quote": "parents want to reach their children",
"comment": "The there/their mix-up from the first draft is corrected here."
},
{
"quote": "teachers cannot watch every desk at once",
"comment": "The missing apostrophe in 'cant' from the first draft is fixed by spelling out 'cannot'."
}
],
"still": "Nothing significant remains; keep proofreading future drafts the same way."
}
],
"next_step": "Move the emergencies/counterargument paragraph so it comes right before your conclusion, after the cyberbullying paragraph, so readers see your full case before you address the objection."
}
College literary analysis - feedback
Input:
{
"task": "feedback",
"title": "The Green Light as a Moving Target in The Great Gatsby",
"assignment": "Choose one symbol in The Great Gatsby and argue how its meaning changes over the course of the novel. Use close reading of at least three passages.",
"essay_type": "Literary analysis",
"level": "First-year college",
"word_limit": "at most 650 words",
"rubric": "",
"teacher_notes": "Your last essay summarised the plot too much. Focus on analysis.",
"feedback_language": "the same language as the essay",
"essay": "The Green Light as a Moving Target in The Great Gatsby\n\nIn F. Scott Fitzgerald's The Great Gatsby, the green light at the end of Daisy's dock is one of the most famous symbols in American literature. The light represents Gatsby's hopes and dreams. Throughout the novel, Fitzgerald uses the green light to show that Gatsby's dream is always out of reach, and this shows the theme of the American Dream.\n\nThe first time we see the green light, Nick sees Gatsby standing on his lawn at night. Gatsby \"stretched out his arms toward the dark water in a curious way,\" and Nick sees \"a single green light, minute and far away.\" Gatsby is reaching for something he can't touch. The light is small and far away, which shows how distant Daisy is from Gatsby. This scene happens at the end of chapter one and it introduces Gatsby as a mysterious character.\n\nIn chapter five Gatsby finally meets Daisy again at Nick's house. After they are reunited, Nick says that the \"colossal significance of that light had now vanished forever.\" Now that Gatsby has Daisy, the light is \"again a green light on a dock.\" This is important because it shows that the dream was more important to Gatsby than the real Daisy. Once he has her the magic is gone. Gatsby shows Daisy his house and his shirts and she cries.\n\nAt the end of the novel Nick connects the green light to everyone. He says Gatsby \"believed in the green light, the orgastic future that year by year recedes before us.\" The word \"recedes\" shows that the future keeps moving away. Nick says \"we\" instead of \"he,\" so the dream is not only Gatsby's but America's. The final image of boats \"borne back ceaselessly into the past\" shows that people keep trying even though they can't win.\n\nIn conclusion, the green light is a very important symbol in The Great Gatsby. It shows Gatsby's dream, it changes when he meets Daisy, and at the end it represents the American Dream. Fitzgerald uses the green light to show that dreams are always out of reach.",
"facts": "words 345 (title line not counted); sentences 23; paragraphs 5; average sentence 15 words; longest sentence 27 words; sentences of 35+ words 0; Flesch reading ease 76.4 (easy), grade level 6.3; passive constructions about 1; overused words [light x13, gatsby x12, green x9, shows x7, daisy x6, dream x6, nick x5, gatsby's x4]; repeated sentence openers [the x5]; filler phrases [very x1]; word limit at most 650 words: WITHIN"
}
Reply:
{
"task": "feedback",
"overview": "This essay traces the green light's meaning across three well-chosen passages (Chapter 1, Chapter 5, and the ending), meeting the assignment's basic requirements. Your strongest move is the close attention to word choice in the final paragraph, especially the shift from 'he' to 'we.' Compared to your teacher's earlier note about summarizing too much, this draft is mostly analytical, but one body paragraph slips back into plot summary. The most important revision is to sharpen your thesis so it explains how the light's meaning changes, not just that the dream stays out of reach.",
"strengths": [
"Your reading of 'recedes' and the pronoun shift from 'he' to 'we' in the final paragraph connects a grammatical detail to the essay's larger claim about America.",
"You chose three passages that map cleanly onto the assignment's requirement, one from early, middle, and late in the novel.",
"The second paragraph connects Gatsby's outstretched arms to the physical distance between him and Daisy, grounding an abstract claim in a concrete image."
],
"criteria": [
{
"id": "thesis",
"band": 3,
"summary": "Your thesis names the symbol and states a claim that you follow through the whole essay, so the paper doesn't wander. But the claim is closer to 'the dream is always distant' than to the change your title promises, so the essay's shape isn't predicted by the opening.",
"quotes": [
{
"quote": "The light represents Gatsby's hopes and dreams.",
"comment": "This states what the symbol stands for but not yet how its meaning will change, which the assignment asks for."
},
{
"quote": "Gatsby's dream is always out of reach, and this shows the theme of the American Dream.",
"comment": "This frames the light as static ('always'), which sits uneasily with the body paragraphs' claim that its meaning shifts after Gatsby and Daisy reunite."
}
],
"suggestion": "In your last intro sentence, add a clause naming the shift you trace (distant hope -> ordinary object -> collective dream) so the thesis previews the essay's arc, not just its topic."
},
{
"id": "structure",
"band": 3,
"summary": "You give the light its own paragraph at three points in the novel, so each paragraph has a clear job. Topic sentences are mostly chronological signposts rather than sentences that argue a sub-point, and the conclusion lists rather than develops your claim.",
"quotes": [
{
"quote": "The first time we see the green light, Nick sees Gatsby standing on his lawn at night.",
"comment": "This topic sentence announces when the scene happens but not what this paragraph will argue about the light's meaning."
},
{
"quote": "In conclusion, the green light is a very important symbol in The Great Gatsby.",
"comment": "This restates the topic rather than closing with the significance of the change you traced."
}
],
"suggestion": "Rewrite your conclusion's first sentence to answer 'so what' - why the light's shifting meaning matters to how we read the ending - instead of listing the three stages again."
},
{
"id": "evidence",
"band": 3,
"summary": "You quote from all three required passages and explain some of them well, such as the link between 'recedes' and a future that keeps moving away. One paragraph, though, drifts into retelling what happens instead of analyzing it, echoing your teacher's earlier note.",
"quotes": [
{
"quote": "the orgastic future that year by year recedes before us",
"comment": "You connect this word choice directly to your claim about dreams staying out of reach - this is your strongest piece of analysis."
},
{
"quote": "Gatsby shows Daisy his house and his shirts and she cries.",
"comment": "This sentence reports plot without explaining what it shows about the green light's meaning, the habit your teacher flagged before."
}
],
"suggestion": "Cut or shorten the plot-summary sentence about the house and shirts, and instead explain what Daisy's reaction suggests about Gatsby's dream now that it has collided with reality."
},
{
"id": "style",
"band": 2,
"summary": "Sentences are clear and easy to follow, but the analysis leans on the same verb ('shows') seven times and the same sentence opener ('The...') five times, per the word counts, which makes the argument feel repetitive rather than built in stages.",
"quotes": [
{
"quote": "This is important because it shows that the dream was more important to Gatsby than the real Daisy.",
"comment": "'Shows' does a lot of work here and elsewhere; varying it would make each claim feel more distinct."
},
{
"quote": "the green light is a very important symbol in The Great Gatsby",
"comment": "'Very important' is vague filler; naming the specific kind of importance would sharpen this sentence."
}
],
"suggestion": "Scan the essay for 'shows' and replace at least half of them with more precise verbs (reveals, undercuts, signals, complicates) chosen to fit each specific claim."
},
{
"id": "mechanics",
"band": 2,
"summary": "Grammar is mostly clean, but a few sentences drop the comma after an introductory phrase, and the essay never cites a chapter or page number for its quotations, which college-level analysis expects.",
"quotes": [
{
"quote": "In chapter five Gatsby finally meets Daisy again at Nick's house.",
"comment": "This introductory phrase needs a comma after 'five' before the main clause."
},
{
"quote": "Once he has her the magic is gone.",
"comment": "This also needs a comma after 'her' to separate the introductory clause from the main clause."
}
],
"suggestion": "Add a comma after every introductory phrase like 'In chapter five' or 'Once he has her,' and add a page or chapter citation in parentheses after each quoted passage."
}
],
"priorities": [
"Sharpen the thesis so it previews how the green light's meaning changes, not just that the dream stays distant.",
"Replace the plot-summary sentence about Gatsby's house and shirts with analysis of what it reveals.",
"Add citations for your quotes and vary your use of 'shows.'"
],
"prompt_fit": "You pick one symbol and use three passages spanning the novel, meeting the assignment's basic requirements, but the argument about how the meaning changes needs to be clearer in the thesis itself, not just visible across paragraphs.",
"rubric_notes": [],
"questions": [
"If a reader only read my thesis, would they know the green light's meaning changes - and how?",
"Which sentences in my body paragraphs describe what happens versus explain what it means?",
"Where would a page or chapter citation help a reader find the passage I'm quoting?"
]
}
Photo of a handwritten page - transcribe
Input:
{
"task": "transcribe",
"pages": 1,
"feedback_language": "the same language as the essay",
"$files": [
"fil_..."
]
}
Reply:
{
"task": "transcribe",
"text": "The Day I Lost the Race\n\nLast spring I ran the 800 meters at the district meet. I had trained for weeks and I was sure I would win. When the gun went off I sprinted to the front, which was a mistake. By the second lap my legs felt like concrete and three girls past me.\n\nI finished fifth. At first I was embarassed and I didnt want to talk to anyone. Then my coach told me that I had run the first lap faster than my best 400. I realized I hadnt lost because I was slow, I lost because I had no plan.",
"legibility": "good",
"unclear": [],
"note": "The handwriting on this one page was clear and fully readable. I ignored a red-ink teacher mark circling the word 'past' with 'sp?' next to it, as instructed."
}
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. 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://essay-feedback.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://essay-feedback.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// essay-feedback.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://essay-feedback.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://essay-feedback.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// EssayFeedback.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class EssayFeedback {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://essay-feedback.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://essay-feedback.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://essay-feedback.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://essay-feedback.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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":"essay-feedback"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "essay-feedback"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "essay-feedback" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "essay-feedback"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "essay-feedback"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "essay-feedback" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "essay-feedback"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "essay-feedback" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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, e.g. {"task": "feedback", "essay": "...", "level": "...", "facts": "..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # the run input object - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "feedback", essay, level, facts, ...} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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 essay-feedback:<task>:<inputHash>:a<attempt>, and its one automatic reformat retry is a new attempt number, named in the page as one extra run. The job output is one JSON object; the code prints the overview and each criterion (a feedback reply has band and suggestion, a revision reply after and still). hold_credits from step 4 is reserved and released; charged_credits is what the run cost. If truncated is true the balance cut the reply short.
KEY="essay-feedback:$(jq -r .task input.json):$(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}'
jq -r '.overview, (.criteria[] | "\(.id): band \(.band // .after) - \(.suggestion // .still)")' reply.json
import hashlib, time
key = "essay-feedback:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("overview"))
for c in reply.get("criteria", []): # feedback: band + suggestion; revision: after + still
print(c["id"], c.get("band", c.get("after")), "-", c.get("suggestion", c.get("still")))
import { createHash } from "node:crypto";
const key = `essay-feedback:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.overview);
for (const c of reply.criteria || []) console.log(c.id, c.band ?? c.after, "-", c.suggestion ?? c.still);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "essay-feedback:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["overview"])
crit, _ := reply["criteria"].([]any)
for _, x := range crit {
c := x.(map[string]any)
fmt.Println(c["id"], c["band"], c["after"], "-", c["suggestion"], c["still"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "essay-feedback:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("overview").asText());
for (JsonNode c : reply.path("criteria"))
System.out.println(c.path("id").asText() + " " + c.path("band").asText(c.path("after").asText()) + " - " + c.path("suggestion").asText(c.path("still").asText()));
require "digest"
key = "essay-feedback:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["overview"]].inspect
(reply["criteria"] || []).each { |c| puts "#{c["id"]} #{c["band"] || c["after"]} - #{c["suggestion"] || c["still"]}" }
$key = "essay-feedback:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["overview"] ?? ""), "\n";
foreach ($reply["criteria"] ?? [] as $c) echo $c["id"], " ", $c["band"] ?? $c["after"], " - ", $c["suggestion"] ?? $c["still"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"essay-feedback:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
Console.WriteLine($"{j["charged_credits"]} {reply["overview"]}");
foreach (var c in reply["criteria"] as JsonArray ?? new JsonArray())
Console.WriteLine($"{c!["id"]} {c["band"] ?? c["after"]} - {c["suggestion"] ?? c["still"]}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json and key. Browsers receive progress ticks rather than text deltas; the final done event carries the job with the whole reply.
# Server-Sent Events: a job event, progress ticks, then a done event carrying the job and its output
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: essay-feedback:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(inp).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "essay-feedback:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(input), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `essay-feedback:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n")) if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
body, _ := json.Marshal(input) // add "bufio", "bytes", "net/http", "strings" to the imports
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", key)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<22)
for sc.Scan() {
if line := sc.Text(); strings.HasPrefix(line, "data:") { fmt.Println(strings.TrimSpace(line[5:])) }
}
var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(BASE + "/run-stream"))
.header("Content-Type", "application/json").header("Authorization", "Bearer " + token).header("Idempotency-Key", key)
.POST(java.net.http.HttpRequest.BodyPublishers.ofByteArray(M.writeValueAsBytes(input))).build();
java.net.http.HttpClient.newHttpClient().send(req, java.net.http.HttpResponse.BodyHandlers.ofLines()).body()
.filter(l -> l.startsWith("data:")).forEach(l -> System.out.println(l.substring(5).trim()));
require "net/http"
uri = URI(BASE + "/run-stream")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}", "Idempotency-Key" => key)
req.body = JSON.generate(input)
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l[5..].strip if l.start_with?("data:") } } }
end
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Content-Type: application/json", "Authorization: Bearer $TOKEN", "Idempotency-Key: $key"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo trim(substr($l, 5)), "\n"; return strlen($chunk); }]);
curl_exec($ch);
using var http = new HttpClient();
var msg = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(input.ToJsonString(), System.Text.Encoding.UTF8, "application/json") };
msg.Headers.Add("Authorization", "Bearer " + Api.Token);
msg.Headers.Add("Idempotency-Key", key);
using var res = await http.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead);
using var rd = new StreamReader(await res.Content.ReadAsStreamAsync());
while (await rd.ReadLineAsync() is string line) if (line.StartsWith("data:")) Console.WriteLine(line[5..].Trim());