Drive Promoted to Big from your own code
The one metered task, pack, is available over HTTP. It takes the child's first name and the page's facts: the child's age and role (big sister, big brother or big sibling), the baby's name and when the baby arrives, who signs, what the child loves, the duties the page picked from its fixed age-safe list, the other changes at home, what stays the same, any worry noticed and the grown-ups-only jobs. It returns one approach (reassure_first, steady_anchors or celebrate), a job title, the greeting, an opening, the offer, the start line, a how-to line per duty, the things that stay the same in the child's words, a reassurance, the sign-off and a short note for the grown-up.
The division of labour first: the page owns the duty list, every pick, every fact and the order; the model owns only the wording; the page then re-checks the reply against its own picks. Lifting, carrying, feeding and bathing the baby are never a child's duty. General, playful ideas - not professional advice.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"promoted-to-big"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, child, page_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: writing the personal letter is metered and needs a personal token. |
| 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. |
The task field and the approaches
Every run sends task: pack (the one task is pack); a missing or different one is still answered as pack. The verdict field holds the approach, by a fixed check order that stops at the first that fits: reassure_first when the worry line says anything other than none noted; steady_anchors when 2 or more other changes are ticked; otherwise celebrate. The page sends its own answer as page_verdict.
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always pack. |
child | yes | The child's first name, up to 24 characters. |
page_facts | yes | One key | ... per line: child (name, age 2 to 8, role), baby (the name, or no name given - write: the baby, and the arrival: already here, in the next few weeks, in a few months or later on), signed_by, loves, three to five duty_<n> lines (short label and the duty), changes (count and list), one or more stays_<n> lines, worry, grown_ups_only, page_verdict and verdict_rule. |
clip_note | no | What the page shortened, if anything (a worry note over 240 characters keeps its start and end). |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "pack", "verdict": "celebrate", "verdict_reason": "...", "job_title": "Big sister and Chief Singer",
"greeting": "Dear Ruby,", "opening": "...", "offer": "...", "start_line": "...",
"duties": [{"n": 1, "duty": "Sing a song to the baby", "how": "..."}], "stays_same": [{"n": 1, "line": "..."}],
"reassurance": "...", "signoff": "With love, Mama and Papa", "grown_up_note": "..."}
| Field | What it holds |
|---|---|
verdict, verdict_reason | The approach id by the check order above; at most 30 words on what decided it. |
job_title, greeting | The role with a capital first letter, plus at most one playful specialty from what the child loves (at most 6 words); "Dear <child>,". |
opening, offer, start_line | At most 40, 40 and 25 words: the news (or, for reassure first, a gentle nod to the worry first), the offer of the job to the child, and when the job starts, with the arrival phrase as written. |
duties | Exactly one entry per picked duty, in order: n, the page's duty text, and a how line of at most 18 words that never touches a grown-ups-only job. |
stays_same | Exactly one entry per stays_<n> line, at most 16 words each, meaning unchanged. |
reassurance, signoff, grown_up_note | At most 35 words fitted to the approach; a sign-off ending with signed_by; at most 40 words for the grown-up, ending "Stay close for every duty near the baby." |
The page then checks the reply (PromoKit.check): the approach against its order, the role in the job title, the child in the greeting and the signer in the sign-off, the arrival phrase in the start line, that the job is offered to the child (never the baby), one entry per duty with the page's own text and no grown-ups-only word in a how line, one line per stays-the-same item, the reassurance fitted to the approach, pronouns, numbers the form does not have, names the form never uses, leaked field ids and the word limits.
Worked examples
These are the three free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover all three approaches - a three-year-old with one change coming (celebrate), a five-year-old with three changes at once (steady anchors), and a seven-year-old who keeps asking about time together, with the baby already here (reassure first).
Ruby, 3, one change coming - celebrate
Input:
{
"task": "pack",
"child": "Ruby",
"page_facts": "child | Ruby | age 3 | becoming: big sister\nbaby | no name given - write: the baby | arrival: in a few months\nsigned_by | Mama and Papa\nloves | singing, teddies and splashing in puddles\nduty_1 | Towel passer | Pass the towel to a grown-up at the baby's bath time\nduty_2 | Song singer | Sing a song to the baby\nduty_3 | Toy picker | Pick a soft toy to show the baby\nchanges | 1 | moving to a big bed\nstays_1 | we love you just as much as ever\nstays_2 | you can tell us any feeling, even cross or sad ones\nworry | none noted\ngrown_ups_only | lifting or carrying the baby; feeding the baby; bath water and bathing; anything in the cot or crib; looking after the baby alone\npage_verdict | celebrate\nverdict_rule | reassure_first when a worry is noted; else steady_anchors when 2 or more changes are ticked; else celebrate"
}
Reply:
{
"task": "pack",
"verdict": "celebrate",
"verdict_reason": "No worry was noted and only 1 change is ticked, so this is a celebrate.",
"job_title": "Big sister and Chief Singer",
"greeting": "Dear Ruby,",
"opening": "Big news! You are being promoted to big sister.",
"offer": "We are pleased to offer you the job of big sister to the baby, because you love singing.",
"start_line": "Your job starts when the baby arrives, in a few months.",
"duties": [
{
"n": 1,
"duty": "Pass the towel to a grown-up at the baby's bath time",
"how": "You bring the towel to a grown-up, so it is ready when the baby needs it."
},
{
"n": 2,
"duty": "Sing a song to the baby",
"how": "You sing softly near the baby, so the baby hears your gentle voice."
},
{
"n": 3,
"duty": "Pick a soft toy to show the baby",
"how": "You choose a toy you like and show it to the baby, so the baby sees something fun."
}
],
"stays_same": [
{
"n": 1,
"line": "We love you just as much as ever."
},
{
"n": 2,
"line": "You can tell us any feeling, even cross or sad ones."
}
],
"reassurance": "Any feeling about the baby is fine, even cross or sad ones, and you can always tell Mama and Papa.",
"signoff": "With love, Mama and Papa.",
"grown_up_note": "Let Ruby sign or draw on the letter and pin it up. Stay close for every duty near the baby."
}
Theo, 5, three changes at once - steady anchors
Input:
{
"task": "pack",
"child": "Theo",
"page_facts": "child | Theo | age 5 | becoming: big brother\nbaby | called: Pip | arrival: in the next few weeks\nsigned_by | Mum\nloves | dinosaurs, drawing and helping\nduty_1 | Welcome-card artist | Draw a welcome picture for the baby\nduty_2 | Nappy fetcher | Fetch a clean nappy when a grown-up asks\nduty_3 | Animal-noise expert | Show the baby animal pictures and make the noises\nduty_4 | Bag packer | Help pack the changing bag before an outing\nchanges | 3 | a new room or sharing a room; starting nursery or school; a relative coming to stay to help\nstays_1 | we love you just as much as ever\nstays_2 | you still get time with just you and a grown-up\nstays_3 | your special toys stay yours\nworry | none noted\ngrown_ups_only | lifting or carrying the baby; feeding the baby; bath water and bathing; anything in the cot or crib; looking after the baby alone\npage_verdict | steady_anchors\nverdict_rule | reassure_first when a worry is noted; else steady_anchors when 2 or more changes are ticked; else celebrate"
}
Reply:
{
"task": "pack",
"verdict": "steady_anchors",
"verdict_reason": "No worry was noted, and 3 changes are ticked, so steady anchors was chosen.",
"job_title": "Big brother and Chief Dinosaur",
"greeting": "Dear Theo,",
"opening": "We have big news for you: you are being promoted to big brother, and we could not be more proud.",
"offer": "We are pleased to offer you the job of big brother to Pip, because you love helping.",
"start_line": "Your job starts when Pip arrives, in the next few weeks.",
"duties": [
{
"n": 1,
"duty": "Draw a welcome picture for the baby",
"how": "Pick happy colours and draw a picture to welcome the baby."
},
{
"n": 2,
"duty": "Fetch a clean nappy when a grown-up asks",
"how": "When asked, bring a clean nappy to the grown-up helping the baby."
},
{
"n": 3,
"duty": "Show the baby animal pictures and make the noises",
"how": "Point to each animal and make its sound to make the baby smile."
},
{
"n": 4,
"duty": "Help pack the changing bag before an outing",
"how": "Help put things into the bag so everything is ready before you go out."
}
],
"stays_same": [
{
"n": 1,
"line": "We love you just as much as ever."
},
{
"n": 2,
"line": "You still get time with just you and a grown-up."
},
{
"n": 3,
"line": "Your special toys stay yours."
}
],
"reassurance": "Some things are changing: a new room or sharing a room, starting nursery or school, and a relative coming to stay to help. The things that stay the same for you, stay the same.",
"signoff": "With love, Mum.",
"grown_up_note": "Whenever a change happens, read the things that stay the same to Theo again so it feels steady. Stay close for every duty near the baby."
}
Ada, 7, a worry about time together, the baby already here - reassure first
Input:
{
"task": "pack",
"child": "Ada",
"page_facts": "child | Ada | age 7 | becoming: big sibling\nbaby | called: Juniper | arrival: already here\nsigned_by | Dad and Papa\nloves | reading, football and making lists\nduty_1 | Bag packer | Help pack the changing bag before an outing\nduty_2 | Story shower | Show the baby a picture book and say what you see\nduty_3 | News reporter | Draw or write the baby's first family newsletter\nduty_4 | Room guide | Tell the baby the names of things around the room\nduty_5 | Quiet-voice captain | Remind everyone to use quiet voices while the baby sleeps\nchanges | 1 | a grown-up going back to work\nstays_1 | you still get time with just you and a grown-up\nstays_2 | you can tell us any feeling, even cross or sad ones\nstays_3 | our bedtime story together\nworry | keeps asking whether we will still have time to read together\ngrown_ups_only | lifting or carrying the baby; feeding the baby; bath water and bathing; anything in the cot or crib; looking after the baby alone\npage_verdict | reassure_first\nverdict_rule | reassure_first when a worry is noted; else steady_anchors when 2 or more changes are ticked; else celebrate"
}
Reply:
{
"task": "pack",
"verdict": "reassure_first",
"verdict_reason": "A worry was noted, so reassure first came first in the order.",
"job_title": "Big sibling and Chief Reader",
"greeting": "Dear Ada,",
"opening": "We know you have been wondering whether we will still have time to read together. And the answer is right here in this letter.",
"offer": "We are pleased to offer you the job of big sibling to Juniper, because you love reading.",
"start_line": "Your job starts now, because Juniper is already here.",
"duties": [
{
"n": 1,
"duty": "Help pack the changing bag before an outing",
"how": "Help pack soft things into the bag so you are ready to go out."
},
{
"n": 2,
"duty": "Show the baby a picture book and say what you see",
"how": "Open the book near the baby and point to pictures while you talk about what you see."
},
{
"n": 3,
"duty": "Draw or write the baby's first family newsletter",
"how": "Drawing or writing tells your family what is happening with the baby."
},
{
"n": 4,
"duty": "Tell the baby the names of things around the room",
"how": "Point to things around the room and say their names out loud for the baby."
},
{
"n": 5,
"duty": "Remind everyone to use quiet voices while the baby sleeps",
"how": "Use a soft voice and gently remind family members while the baby is sleeping."
}
],
"stays_same": [
{
"n": 1,
"line": "You still get time with just you and a grown-up."
},
{
"n": 2,
"line": "You can tell us any feeling, even cross or sad ones."
},
{
"n": 3,
"line": "You still have our bedtime story together."
}
],
"reassurance": "Yes, you will still have time to read together, with our bedtime story staying just the same.",
"signoff": "With love, Dad and Papa.",
"grown_up_note": "Read this letter together at a calm moment and let Ada answer back. Stay close for every duty near the baby."
}
Build the input yourself
Load the engine in node (global.window = global; require("./promokit.js")), then:
const K = PromoKit;
const built = K.buildInput({ child: "Ruby", age: 3, role: "sister", baby: "", arrival: "months", signed: "Mama and Papa",
loves: "singing, teddies and splashing in puddles", changes: ["bed"], stays: ["love", "feelings"], worry: "" });
built.duties.map(d => d.text); // the page's picks for age 3, loves first
built.verdict; // "celebrate" - no worry and only 1 change
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply) // the letter as Markdown
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://promoted-to-big.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://promoted-to-big.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"]
// promoted-to-big.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://promoted-to-big.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://promoted-to-big.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
}
// PromotedToBig.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 PromotedToBig {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://promoted-to-big.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://promoted-to-big.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://promoted-to-big.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://promoted-to-big.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":"promoted-to-big"}' | 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": "promoted-to-big"})["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: "promoted-to-big" })).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": "promoted-to-big"}, 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", "promoted-to-big"), 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: "promoted-to-big" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "promoted-to-big"])["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 = "promoted-to-big" }))!["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 - {"task": "pack", "child": "Ruby", "page_facts": "child | Ruby | age 3 | becoming: big sister\n..."} - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "pack", "child": "Ruby", "page_facts": "child | Ruby | age 3 | becoming: big sister\n..."} - see the field 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: "pack", child, page_facts} (see the field 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 task and the input: a retry with the same key never bills twice. The page uses promoted-to-big:<task>:<inputHash>:a<attempt> (the one task is pack), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="promoted-to-big:pack:$(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}'
import hashlib, time
key = "promoted-to-big:" + 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("job_title"))
import { createHash } from "node:crypto";
const key = `promoted-to-big:${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.job_title);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "promoted-to-big:" + 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 }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "promoted-to-big:" + 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);
require "digest"
key = "promoted-to-big:#{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["job_title"]].inspect
$key = "promoted-to-big:" . $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["job_title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"promoted-to-big:{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}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: promoted-to-big:pack:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "pack", "child": "Ruby", "page_facts": "child | Ruby | age 3 | becoming: big sister\n..."} - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "promoted-to-big:pack:" + hashlib.sha256(json.dumps(rev).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 rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `promoted-to-big:pack:${createHash("sha256").update(JSON.stringify(rev)).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));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "promoted-to-big:pack:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "promoted-to-big:pack:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "promoted-to-big:pack:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: promoted-to-big:pack:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"promoted-to-big:pack:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);