← Seven Kilos
Seven Kilos API
Tokens

Drive Seven Kilos from your own code

The one metered step - the gate check of one traveller's carry-on packing list - is available over HTTP. A natural use is a packing-list spreadsheet or a trip-planning tool: your code sends the list with the page's weigh-in, and gets back the moves (what to wear on the plane, what to cut, swap, move between bags, check or ship), a rewear plan for the days, next steps and a fits / trim / check-a-bag verdict.

The split first: the page weighs; the model judges; the page re-weighs the model's moves. Reading the list, the bag totals against each limit, the wear moves, the clothing counts and the liquid and battery flags come from packkit.js, which runs in node as well as in the browser. Its check function then applies the reply's moves to the bags itself: every move must name a packed line and quote it as written, wearing shoes or a coat puts the pair you planned to wear in the bag, a swap only counts when its estimated weight is lighter, and a "fits" or "trim" verdict that still leaves a bag over is flagged.

Get a token on the token page (step 1 below also shows how to mint a guest token in code).

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":"seven-kilos"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the gate check is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Every run sends task. There is one task, weigh. It returns title, verdict, summary; moves [{item, action, qty, to, says, why, swap_for, swap_g}] (0-12, most weight saved first); rewear (2-5 lines); next_steps (3-5); caveats (0-3).

VerdictMeaning
fitsEvery allowance already has 300 g or more to spare and no liquid gift is in a cabin bag. Moves are optional.
trimOver, within 300 g of a limit, or a liquid gift to handle - and wearing, cutting, swapping and moving bring every allowance under its limit without a checked bag.
checkEven after sensible wearing and cutting (the page's wear moves leave more than 1 kg over) the trip needs a checked bag or shipping: the moves name what goes in the hold or gets shipped.
ActionWhat the page does with it
wearOne piece leaves its bag. For shoes or a coat, the pair or coat already listed as worn goes into that bag instead.
cutqty of the line leave the bag (default: the whole line).
swapEach of qty pieces weighs swap_g instead - only when swap_g is lighter. swap_g is the model's estimate.
moveMoves to the bag named in to (cabin or personal). Changes nothing under a combined allowance.
check / shipLeaves the cabin bags. A spare battery sent to check is flagged.

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

Input fields

FieldTypeNotes
taskstringAlways weigh.
todaystringYYYY-MM-DD.
packingstringThe packing list as pasted. Past 6,000 characters the middle is cut on whole lines around a marker.
factsstring (JSON){trip, nights, days, nights_assumed, laundry, mode, cabin_limit_kg, personal_limit_kg, limit_assumed, cabin_tare_kg, personal_tare_kg, cabin_tare_assumed, personal_tare_assumed, bags{cabin, personal, worn}, checks[{id, label, total_g, limit_g, over_g}], worst_over_g, items_n, units, packed_g, by_kind, heaviest, wear_moves[{item, from, save_g, swap_back}], wear_save_g, after_wear_over_g, clothing{tops, bottoms, dresses, underwear, socks, shoes_pairs}, liquids, gift_liquids, batteries}. over_g above 0 is over the limit; below 0 is grams to spare.
itemsstring (JSON)[{item, unit_g, qty, total_g, bag, kind, liquid, battery}]; bag is cabin, personal or worn.
page_readsstring (JSON)[{level, code, text}]; level is fix or info.
page_verdictstringThe page's own reading: fits, trim or check.
notesstringOptional, the traveller's own comments; past 600 characters the middle is cut.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with PackKit.parseReply, then normalize, then check it with check(reply, state), which returns {rows, applied, notes, totals, after, saved_g}: one row per move with the grams it saves as the page counts them (or why it was not counted), the bag totals and allowance checks after the moves, and the notes (ok, info, warn, bad) the page shows under "Re-weighed by the page".

Worked examples

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

task: weigh - City weekend - fits

Input:

{
 "task": "weigh",
 "today": "2026-10-20",
 "packing": "Merino T-shirt | 140 g | 2 | cabin\nChinos | 450 g | 1 | cabin\nSweater | 380 g | 1 | cabin\nUnderwear | 60 g | 3 | cabin\nSocks | 50 g | 3 | cabin\nToiletry bag, travel sizes | 650 g | 1 | cabin\nJeans | 6 ... (410 characters)",
 "facts": "{\"trip\":\"Long weekend at a friend's flat\",\"nights\":2,\"days\":3,\"nights_assumed\":false,\"laundry\":false,\"mode\":\"separate\",\"cabin_limit_kg\":7,\"personal_limit_kg\":null,\"limit_assumed\":false,\"cabin_tare_kg\":1.6,\"personal_tare_kg\":0.4,\"cabin_tare_assumed\":false,\"personal_tare_assumed\":false,\"bags\":{\"cabin\":{\"items_g\":2090,\"total_g\":3690},\"personal\":{\"items_g\":2130,\"total_g\":2530},\"worn\":{\"total_g\":1650}},\"checks\":[{\"id\":\"cabin\",\"label\":\"Cabin bag\",\"total_g\":3690,\"limit_g\":7000,\"over_g\":-3310}],\"worst_over_g\":-3310,\"items_n\":13,\"units\":18,\"packed_g\":6220,\"by_kind\":{\"clothes\":1440,\"outer\":0,\"shoes\":0,\"toiletries\":650,\"tech\":1850,\"gifts\":0,\"other\":280},\"heaviest\":[{\"item\":\"Laptop\",\"total_g\":1300,\"bag\":\"personal\"},{\"item\":\"Toiletry bag, travel sizes\",\"total_g\":650,\"bag\":\"cabin\"},{\"item\":\"Chinos\",\"total_g\":450,\"bag\":\"cabin\"},{\"item\":\"Sweater\",\"total_g\":380,\"bag\":\"cabin\"},{\"item\":\"Laptop charger\",\"total_g\":300,\"bag\":\"personal\"}],\"wear_moves\":[{\"item\":\"Sweater\",\"from\":\"cabin\",\"save_g\":380,\"swap_back\":null}],\"wear_save_g\":380,\"after_wear_over_g\":-3690,\"clothing\":{\"tops\":3,\"bottoms\":2,\"dresses\":0,\"underwear\":3,\"socks\":3,\"shoes_pairs\":1},\"liquids\":[],\"gift_liquids\":[],\"batteries\":[{\"item\":\"Power bank\",\"bag\":\"personal\"}]}",
 "items": "[{\"item\":\"Merino T-shirt\",\"unit_g\":140,\"qty\":2,\"total_g\":280,\"bag\":\"cabin\",\"kind\":\"clothes\",\"liquid\":false,\"battery\":false},{\"item\":\"Chinos\",\"unit_g\":450,\"qty\":1,\"total_g\":450,\"bag\":\"cabin\",\"kind\":\"clothes\",\"liquid\":false,\"battery\":false},\"... 11 more\"]",
 "page_reads": "[{\"level\":\"info\",\"code\":\"personal_unchecked\",\"text\":\"No personal-item limit entered - it is totalled, not checked.\"}]",
 "page_verdict": "fits",
 "notes": ""
}

Reply:

{
 "task": "weigh",
 "title": "A weekend carry-on with room to spare",
 "verdict": "fits",
 "summary": "This cabin bag already fits: 3.69 kg packed against the 7 kg limit, leaving 3.31 kg to spare, and there are no liquid gifts to route around security. The personal item adds 2.53 kg of its own (unchecked, since no personal-item limit was entered), with the spare battery already riding there instead of in the cabin bag. No changes are needed - three tops and two bottoms already suit a three-day trip with no laundry. The biggest reason it travels this light: the jeans, sneakers and rain jacket are all worn, not packed.",
 "moves": [],
 "rewear": [
  "Day 1 (travel): jeans, sneakers and the rain jacket (all worn) with a merino T-shirt",
  "Day 2: chinos with the second merino T-shirt, sweater layered on top",
  "Day 3: back to the worn jeans with the sweater; fresh underwear and socks each day from the packed three"
 ],
 "next_steps": [
  "Weigh the packed cabin bag at home - it should read close to 3.69 kg, well inside the 7 kg limit.",
  "Wear the jeans, sneakers and rain jacket to the airport, as planned.",
  "Keep the power bank in the personal item, not the cabin bag, in case either is weighed separately at the gate.",
  "..."
 ],
 "caveats": [
  "The personal item's 2.53 kg hasn't been checked against a limit - only the cabin bag was entered.",
  "Gate scales and exact allowances can vary by airline and fare, even though this list already clears the 7 kg cabin limit with room to spare."
 ]
}

task: weigh - Thanksgiving, 4 nights - trim

Input:

{
 "task": "weigh",
 "today": "2026-10-20",
 "packing": "Winter boots | 1150 g | 1 | cabin\nPuffer jacket | 800 g | 1 | cabin\nSweater | 420 g | 2 | cabin\nT-shirt | 160 g | 4 | cabin\nJeans | 650 g | 2 | cabin\nPajamas | 350 g | 1 | cabin\nUnderwear | 60 g | 5 | ... (526 characters)",
 "facts": "{\"trip\":\"Thanksgiving at my parents'\",\"nights\":4,\"days\":5,\"nights_assumed\":false,\"laundry\":true,\"mode\":\"separate\",\"cabin_limit_kg\":7,\"personal_limit_kg\":4,\"limit_assumed\":false,\"cabin_tare_kg\":2.4,\"personal_tare_kg\":0.6,\"cabin_tare_assumed\":false,\"personal_tare_assumed\":false,\"bags\":{\"cabin\":{\"items_g\":6905,\"total_g\":9305},\"personal\":{\"items_g\":1780,\"total_g\":2380},\"worn\":{\"total_g\":1250}},\"checks\":[{\"id\":\"cabin\",\"label\":\"Cabin bag\",\"total_g\":9305,\"limit_g\":7000,\"over_g\":2305},{\"id\":\"personal\",\"label\":\"Personal item\",\"total_g\":2380,\"limit_g\":4000,\"over_g\":-1620}],\"worst_over_g\":2305,\"items_n\":17,\"units\":30,\"packed_g\":11685,\"by_kind\":{\"clothes\":3705,\"outer\":800,\"shoes\":1150,\"toiletries\":1250,\"tech\":1030,\"gifts\":0,\"other\":750},\"heaviest\":[{\"item\":\"Jeans\",\"total_g\":1300,\"bag\":\"cabin\"},{\"item\":\"Winter boots\",\"total_g\":1150,\"bag\":\"cabin\"},{\"item\":\"Sweater\",\"total_g\":840,\"bag\":\"cabin\"},{\"item\":\"Puffer jacket\",\"total_g\":800,\"bag\":\"cabin\"},{\"item\":\"Toiletries\",\"total_g\":700,\"bag\":\"cabin\"}],\"wear_moves\":[{\"item\":\"Winter boots\",\"from\":\"cabin\",\"save_g\":450,\"swap_back\":\"Sneakers\"},{\"item\":\"Puffer jacket\",\"from\":\"cabin\",\"save_g\":800,\"swap_back\":null},{\"item\":\"Jeans\",\"from\":\"cabin\",\"save_g\":650,\"swap_back\":null}],\"wear_save_g\":1900,\"after_wear_over_g\":405,\"clothing\":{\"tops\":7,\"bottoms\":2,\"dresses\":0,\"underwear\":5,\"socks\":5,\"shoes_pairs\":2},\"liquids\":[],\"gift_liquids\":[],\"batteries\":[{\"item\":\"Power bank\",\"bag\":\"personal\"}]}",
 "items": "[{\"item\":\"Winter boots\",\"unit_g\":1150,\"qty\":1,\"total_g\":1150,\"bag\":\"cabin\",\"kind\":\"shoes\",\"liquid\":false,\"battery\":false},{\"item\":\"Puffer jacket\",\"unit_g\":800,\"qty\":1,\"total_g\":800,\"bag\":\"cabin\",\"kind\":\"outer\",\"liquid\":false,\"battery\":false},\"... 15 more\"]",
 "page_reads": "[{\"level\":\"fix\",\"code\":\"over\",\"text\":\"Cabin bag weighs 9.31 kg against a 7.00 kg limit - 2.31 kg over.\"},{\"level\":\"info\",\"code\":\"wear\",\"text\":\"Wearing Winter boots (instead of Sneakers), Puffer jacket and Jeans on the plane takes 1.90 kg off the bags - still 0.41 kg over after that.\"},\"... 1 more\"]",
 "page_verdict": "trim",
 "notes": "They have a washer. It might snow, so I want the boots there."
}

Reply:

{
 "task": "weigh",
 "title": "Trimming the Snowy Thanksgiving Carry-On",
 "verdict": "trim",
 "summary": "Trim: the cabin bag is 9.31 kg against a 7.00 kg limit (2.31 kg over), while the personal item already has room to spare at 2.38 kg against a 4.00 kg limit. Wearing the puffer jacket (800 g), one pair of jeans (650 g) and the winter boots (1150 g, with the 700 g sneakers going into the bag instead for a net 450 g) on the plane saves 1.90 kg but still leaves the cabin bag 0.41 kg over. Cutting 2 of the 4 T-shirts (320 g, trimming 7 tops to the roughly 5 that 5 days with a washer need) and swapping the 700 g toiletries bag for a roughly 300 g travel-size kit (400 g) bring the cabin bag down to about 6.69 kg, 0.32 kg under the limit. The single most useful change is wearing the puffer jacket and boots instead of packing them.",
 "moves": [
  {
   "item": "Puffer jacket",
   "action": "wear",
   "qty": 1,
   "to": "",
   "says": "Puffer jacket | 800 g | 1 | cabin",
   "why": "The puffer jacket is 800 g; wearing it on the plane takes the full weight out of the cabin bag.",
   "swap_for": "",
   "swap_g": null
  },
  {
   "item": "Jeans",
   "action": "wear",
   "qty": 1,
   "to": "",
   "says": "Jeans | 650 g | 2 | cabin",
   "why": "One pair of jeans is 650 g; wearing it on the plane instead of packing both pairs saves that weight straight out of the cabin bag.",
   "swap_for": "",
   "swap_g": null
  },
  {
   "item": "Winter boots",
   "action": "wear",
   "qty": 1,
   "to": "",
   "says": "Winter boots | 1150 g | 1 | cabin",
   "why": "The 1150 g winter boots go on your feet instead of the bag, and the sneakers you'd planned to wear (700 g) go in instead - a net 450 g off the cabin bag, and the boots are with you for the snow.",
   "swap_for": "",
   "swap_g": null
  },
  "..."
 ],
 "rewear": [
  "Day 1 (flight day): hoodie, the jeans you're wearing, puffer jacket on top, boots on your feet.",
  "Days 2-3: a sweater or T-shirt with the packed jeans, swapping the top each day.",
  "Day 4: wash with the washer on hand and repeat day 1's hoodie and jeans.",
  "..."
 ],
 "next_steps": [
  "Pick up or pack the travel-size toiletries kit in place of the full-size one, and leave 2 T-shirts at home.",
  "Lay out the wear-on-plane outfit the night before: puffer jacket, one pair of jeans, winter boots, and the hoodie.",
  "Keep the power bank in the personal item, not the cabin bag, in case the cabin bag gets gate-checked.",
  "..."
 ],
 "caveats": [
  "The travel-size toiletries kit's weight (about 300 g) is an estimate - check it against what you actually pack.",
  "Gate scales and cabin-bag rules vary by airline and fare, so treat this as a guide, not a guarantee."
 ]
}

task: weigh - Winter holidays with gifts - check

Input:

{
 "task": "weigh",
 "today": "2026-10-20",
 "packing": "Wrapped board game for the kids | 1400 g | 1 | cabin\nGift bottle of maple syrup | 500 g | 1 | cabin\nGift sweater for Dad | 600 g | 1 | cabin\nSnow boots | 1300 g | 1 | cabin\nSweater | 420 g | 3 | cabin ... (555 characters)",
 "facts": "{\"trip\":\"Winter holidays with family\",\"nights\":9,\"days\":10,\"nights_assumed\":false,\"laundry\":true,\"mode\":\"combined\",\"cabin_limit_kg\":7,\"personal_limit_kg\":null,\"limit_assumed\":false,\"cabin_tare_kg\":2.2,\"personal_tare_kg\":0.8,\"cabin_tare_assumed\":false,\"personal_tare_assumed\":false,\"bags\":{\"cabin\":{\"items_g\":9110,\"total_g\":11310},\"personal\":{\"items_g\":2850,\"total_g\":3650},\"worn\":{\"total_g\":2100}},\"checks\":[{\"id\":\"both\",\"label\":\"Cabin bag + personal item\",\"total_g\":14960,\"limit_g\":7000,\"over_g\":7960}],\"worst_over_g\":7960,\"items_n\":17,\"units\":36,\"packed_g\":14960,\"by_kind\":{\"clothes\":4510,\"outer\":0,\"shoes\":1300,\"toiletries\":800,\"tech\":2850,\"gifts\":2500,\"other\":0},\"heaviest\":[{\"item\":\"Laptop\",\"total_g\":1500,\"bag\":\"personal\"},{\"item\":\"Wrapped board game for the kids\",\"total_g\":1400,\"bag\":\"cabin\"},{\"item\":\"Snow boots\",\"total_g\":1300,\"bag\":\"cabin\"},{\"item\":\"Jeans\",\"total_g\":1300,\"bag\":\"cabin\"},{\"item\":\"Sweater\",\"total_g\":1260,\"bag\":\"cabin\"}],\"wear_moves\":[{\"item\":\"Snow boots\",\"from\":\"cabin\",\"save_g\":600,\"swap_back\":\"Sneakers\"},{\"item\":\"Jeans\",\"from\":\"cabin\",\"save_g\":650,\"swap_back\":null}],\"wear_save_g\":1250,\"after_wear_over_g\":6710,\"clothing\":{\"tops\":8,\"bottoms\":2,\"dresses\":0,\"underwear\":7,\"socks\":7,\"shoes_pairs\":2},\"liquids\":[\"Gift bottle of maple syrup\"],\"gift_liquids\":[\"Gift bottle of maple syrup\"],\"batteries\":[{\"item\":\"Power bank\",\"bag\":\"personal\"}]}",
 "items": "[{\"item\":\"Wrapped board game for the kids\",\"unit_g\":1400,\"qty\":1,\"total_g\":1400,\"bag\":\"cabin\",\"kind\":\"gifts\",\"liquid\":false,\"battery\":false},{\"item\":\"Gift bottle of maple syrup\",\"unit_g\":500,\"qty\":1,\"total_g\":500,\"bag\":\"cabin\",\"kind\":\"gifts\",\"liquid\":true,\"battery\":false},\"... 15 more\"]",
 "page_reads": "[{\"level\":\"fix\",\"code\":\"over\",\"text\":\"Cabin bag + personal item weighs 14.96 kg against a 7.00 kg limit - 7.96 kg over.\"},{\"level\":\"info\",\"code\":\"wear\",\"text\":\"Wearing Snow boots (instead of Sneakers) and Jeans on the plane takes 1.25 kg off the bags - still 6.71 kg over after that.\"},\"... 3 more\"]",
 "page_verdict": "check",
 "notes": "One strict 7 kg allowance for both bags on this ticket."
}

Reply:

{
 "task": "weigh",
 "title": "Winter Holiday Carry-On Needs A Checked Bag",
 "verdict": "check",
 "summary": "This is a sensible 10-day winter list with laundry, but it's being weighed against an unusually strict combined 7.00 kg allowance covering the cabin bag and personal item together, and as packed it comes to 14.96 kg - 7.96 kg over. Wearing the snow boots and one pair of jeans saves 1.25 kg (6.71 kg still over), and shipping the three gifts ahead while checking the rest of the clothes and the toiletries saves another 7.16 kg, landing the combo at 6.55 kg - 450 g to spare under the 7.00 kg limit. The single biggest driver is structural, not the packing itself: the two empty bags already weigh 3.00 kg and the laptop, charger, camera and power bank add 2.85 kg, leaving almost no room in a 7 kg combined limit for any packed clothes at all.",
 "moves": [
  {
   "item": "Wrapped board game for the kids",
   "action": "ship",
   "qty": 1,
   "to": "",
   "says": "Wrapped board game for the kids | 1400 g | 1 | cabin",
   "why": "At 1400 g this is the single heaviest line in the bag; mailing it ahead keeps the full weight out of the gate weigh-in.",
   "swap_for": "",
   "swap_g": null
  },
  {
   "item": "Sweater",
   "action": "check",
   "qty": 3,
   "to": "",
   "says": "Sweater | 420 g | 3 | cabin",
   "why": "Three sweaters at 420 g each (1260 g) are the next-heaviest block; checking all three clears it since a checked bag is coming anyway.",
   "swap_for": "",
   "swap_g": null
  },
  {
   "item": "Toiletries",
   "action": "check",
   "qty": 1,
   "to": "",
   "says": "Toiletries | 800 g | 1 | cabin",
   "why": "Toiletries weigh 800 g; nothing about them is needed in the air, so checking the whole line takes 800 g off the combined total.",
   "swap_for": "",
   "swap_g": null
  },
  "..."
 ],
 "rewear": [
  "Days 1-2: the jeans worn onto the plane plus a sweater, thermals underneath if it's cold; first wash around day 3.",
  "Days 3-5: second sweater over a t-shirt, the same jeans back in rotation after the wash.",
  "Days 6-8: third sweater or t-shirts alone indoors; wash underwear and socks as needed from the 7 of each.",
  "..."
 ],
 "next_steps": [
  "Weigh the cabin bag and personal item together at home before you leave - you're targeting about 6.55 kg against the 7.00 kg combined limit.",
  "Set the board game, the maple syrup and Dad's sweater aside to ship or mail ahead rather than packing them.",
  "Move the sweaters, the spare jeans, the t-shirts, thermals, underwear, socks and toiletries into the checked bag.",
  "..."
 ],
 "caveats": [
  "Liquid rules for the maple syrup vary by airport - shipping sidesteps the question, but checking it instead is also fine.",
  "This assumes the checked bag's own allowance has room for the added weight - confirm that with the airline separately."
 ]
}

Build the input yourself

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

const K = PackKit;
const st = K.state({
  today: "2026-10-20", trip: "Thanksgiving", nights: "4", laundry: "yes", mode: "separate",
  cabin: "7", personal: "", cabin_tare: "2.4", personal_tare: "0.6", notes: "",
  items: "Winter boots | 1150 g | 1 | cabin\nT-shirt | 160 g | 4 | cabin\nSneakers | 700 g | 1 | worn"
});
console.log(st.checks, st.facts.wear_moves, st.verdict);
const { input } = K.buildInput(st, { today: "2026-10-20" });
const wire = K.toWire(input);           // POST this to /estimate and /run
// later: const reply = K.normalize(K.parseReply(job.output.output));
// const { rows, after, notes } = K.check(reply, st);

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://seven-kilos.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"seven-kilos"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "weigh", "today": "2026-10-20", "packing": "Winter boots | 1150 g | 1 | cabin ...", "facts": "{...}", "items": "[...]", "page_reads": "[...]", "page_verdict": "trim"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses seven-kilos:weigh:<inputHash>:a<attempt>, and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="seven-kilos:weigh:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: seven-kilos:weigh:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json