← Two Ways Out
API
Tokens

Drive Two Ways Out from your own code

The one metered task, premortem, is available over HTTP. It takes a home fire escape plan - rooms with their level, door route, window and smoke alarm; people with the room they sleep in, what they need and who helps them; the household's answers on alarms, drills and the meeting spot - together with the page's own findings, and returns a verdict (rework, patch or drill), three to six ways the plan could fail at night (each tied to a finding or to the notes, with a severity and one concrete fix), a practice-drill script for the household, one to three open questions, and one thing the plan already does well. A natural use is a home-safety checklist, a landlord's move-in pack or a family organiser that keeps a plan and wants it stress-tested.

The division of labour first: the page owns the checks; the model owns the judgement and the words; the page re-checks the reply. Counting each room's ways out, matching people to rooms and helpers, the blockers and gaps, the verdict rule and the rough escape clock come from waykit.js, which runs in node as well as in the browser. WayKit.check holds each reply to them: the verdict by the rule, a failure for every blocker marked high, real finding ids whose room or person matches, failures from notes never high, and no number, capitalised name, he/she or internal id that the input does not hold.

Get a token on the token page (step 2 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":"two-ways-out"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, rooms, people, household, facts, figures) 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 premortem is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field and the verdict rule

Every run sends task: premortem (the only task; a missing or unknown one is treated as premortem). The verdict is decided by a fixed rule on the page's figures, checked in this order - the first that applies wins:

  1. rework - the page found 1 or more blockers (a slept-in room with no usable way out, a person who needs help with nobody named to help, no smoke alarm confirmed anywhere).
  2. patch - the page found 1 or more gaps (one way out, no alarm inside a slept-in room, a child nobody is named to wake, a helper on another floor, a person not matched to a room, no alarm confirmed on every level, alarms not tested this month, no meeting spot, no drill in the last six months).
  3. drill - everything else.

The declared input schema carries scalars only, so the array- and object-valued fields travel JSON-encoded as strings (WayKit.toWire does this). Every value is a string.

Input fields

FieldTypeNotes
taskstringpremortem. Required.
roomsstring (JSON)[{id, name, level, door, window, alarm, ways_out, sleepers}] - ids R1.., up to 12; level, door, window, alarm in plain words; ways_out the page's count; sleepers person ids. Required.
peoplestring (JSON)[{id, name, room, needs, helper}] - ids P1.., up to 10; room and helper are ids or empty; needs in plain words. Required.
householdstring (JSON){alarm_every_level, alarms_last_tested, last_drill, timed_drill_seconds, meeting_spot}, plain words. Required.
factsstring (JSON)[{id, level, ref, text}] - the page's findings, ids F1..; level is blocker or gap; ref a room id, a person id or H. Required (may be []).
figuresstring (JSON){blockers, gaps, rooms, sleeping_rooms, people}, every value a string. Required.
notesstringFree text from the household, up to 300 characters.
clip_note, retry_notestringWhat the page cut, when it cut something; and only on a reformat retry, what was wrong.

The reply

The job's output.output is a string holding one JSON object: task, verdict, title, summary, failures (3-6 {fact, ref, story, severity, fix}, severity high for blockers, medium for gaps, medium or low for failures from the notes alone), drill (4-7 strings), questions (1-3 strings) and keep. Parse with WayKit.parseReply, then normalize(reply), then check(reply, built) where built = WayKit.buildInput(form); md(reply, built, form) writes the Markdown, csv(built) the room sheet and ics(date, meetingSpot) the reminders calendar.

Worked examples

Replies produced against the app's prompt for the three sample homes (made up; long fields shortened; the full replies ship in example-result.js). Together they cover every verdict.

a family house where someone who needs help has no helper - rework

Input:

{
 "task": "premortem",
 "rooms": "[{\"id\":\"R1\",\"name\":\"Main bedroom\",\"level\":\"one floor up\",\"door\":\"a door to a hall or stairs that lead outside\",\"window\":\"an upper window with no ladder\",\"alarm\":\"a smoke alarm inside\",\"ways_out\":1,\"sleepers\":[\"P1\",\"P2\"]},{\"id\":\"R2\",\"name\":\"Kids' room\",\"level\":\"one floor up\",\"door\":\"a door to a hall or stairs that lead outside\",\"window\":\"an upper window with no ladder\",\"alarm\":\"no smoke alarm inside\",\"ways_out\":1,\"sle ... (1109 characters)",
 "people": "[{\"id\":\"P1\",\"name\":\"Dana\",\"room\":\"R1\",\"needs\":\"gets out alone\",\"helper\":\"\"},{\"id\":\"P2\",\"name\":\"Sam\",\"room\":\"R1\",\"needs\":\"gets out alone\",\"helper\":\"\"},{\"id\":\"P3\",\"name\":\"Theo\",\"room\":\"R2\",\"needs\":\"a child who may sleep through an alarm\",\"helper\":\"P1\"},{\"id\":\"P4\",\"name\":\"Ivy\",\"room\":\"R2\",\"needs\":\"a child who may sleep through an alarm\",\"helper\":\"\"},{\"id\":\"P5\",\"name\":\"Ruth\",\"room\":\"R3\",\"needs\":\"needs help to get out (a  ... (554 characters)",
 "household": "{\"alarm_every_level\":\"not sure\",\"alarms_last_tested\":\"more than six months ago\",\"last_drill\":\"never\",\"timed_drill_seconds\":\"\",\"meeting_spot\":\"\"}",
 "facts": "[{\"id\":\"F1\",\"level\":\"blocker\",\"ref\":\"P5\",\"text\":\"Ruth needs help to get out and nobody is named to help.\"},{\"id\":\"F2\",\"level\":\"gap\",\"ref\":\"R1\",\"text\":\"Main bedroom has one way out (the door; an upper window with no ladder).\"},{\"id\":\"F3\",\"level\":\"gap\",\"ref\":\"R2\",\"text\":\"Kids' room has one way out (the door; an upper window with no ladder).\"},{\"id\":\"F4\",\"level\":\"gap\",\"ref\":\"R4\",\"text\":\"Basement bedroom has one way out  ... (1337 characters)",
 "figures": "{\"blockers\":\"1\",\"gaps\":\"11\",\"rooms\":\"5\",\"sleeping_rooms\":\"4\",\"people\":\"6\"}",
 "notes": "The front door deadbolt needs a key; the key hangs on a hook by the door. Ruth uses a walker. The basement window was painted shut years ago.",
 "clip_note": ""
}

Reply:

{
 "task": "premortem",
 "verdict": "rework",
 "title": "Ruth has no named helper - fix that first",
 "summary": "This plan needs rework, with 1 blocker and 11 gaps to address. The most urgent step is to name someone who can help Ruth get out of the Den.",
 "failures": [
  {
   "fact": "F1",
   "ref": "P5",
   "story": "Ruth uses a walker and needs help to get out of the Den, but no one is named to help them get out safely.",
   "severity": "high",
   "fix": "Name a person to wake and help Ruth get out of the Den."
  },
  {
   "fact": "F8",
   "ref": "P4",
   "story": "Ivy may sleep through the smoke alarm in the Kids' room, and nobody is named to wake them.",
   "severity": "medium",
   "fix": "Name someone to wake Ivy if the alarm sounds, so they do not sleep through it."
  },
  {
   "fact": "F5",
   "ref": "R2",
   "story": "The Kids' room where Theo and Ivy sleep has no smoke alarm inside, so a fire there might not wake them in time.",
   "severity": "medium",
   "fix": "Put a smoke alarm inside the Kids' room where Theo and Ivy sleep."
  },
  {
   "fact": "F7",
   "ref": "R4",
   "story": "The Basement bedroom where Marco sleeps has no smoke alarm inside, so smoke might spread before Marco wakes.",
   "severity": "medium",
   "fix": "Put a smoke alarm inside the Basement bedroom where Marco sleeps."
  },
  {
   "fact": "F4",
   "ref": "R4",
   "story": "The Basement bedroom has only the door as a way out, since the window was painted shut, so Marco could be trapped if the door is blocked.",
   "severity": "medium",
   "fix": "Free the painted-shut window in the Basement bedroom so Marco has a second way out."
  },
  {
   "fact": "",
   "ref": "H",
   "story": "The front door needs a key kept on a hook by the door, so finding it in smoke and dark could slow everyone's way out.",
   "severity": "medium",
   "fix": "Make sure everyone in the household knows exactly where the key is kept."
  }
 ],
 "drill": [
  "Whoever notices smoke or hears the alarm first wakes the rest of the household right away.",
  "Dana and Sam leave the Main bedroom through the door.",
  "In the Kids' room, Dana helps Theo out the door; choose someone to wake and help Ivy the same way.",
  "In the Den, choose someone to help Ruth out the door or the ground-level window.",
  "Marco leaves the Basement bedroom through the door.",
  "Everyone meets at a spot outside that you choose before the drill.",
  "Time how long the whole drill takes, since there is no time to beat yet."
 ],
 "questions": [
  "Who will be named to help Ruth get outside if nobody is assigned yet?",
  "Who will wake Ivy if they do not hear the alarm?",
  "Where outside will everyone meet after getting out of the house?"
 ],
 "keep": "Dana is already named to help Theo get out of the Kids' room."
}

a shared top-floor flat with one bedroom on one way out - patch

Input:

{
 "task": "premortem",
 "rooms": "[{\"id\":\"R1\",\"name\":\"Front bedroom\",\"level\":\"two floors up\",\"door\":\"a door to a hall or stairs that lead outside\",\"window\":\"an upper window with no ladder\",\"alarm\":\"a smoke alarm inside\",\"ways_out\":1,\"sleepers\":[\"P1\"]},{\"id\":\"R2\",\"name\":\"Back bedroom\",\"level\":\"two floors up\",\"door\":\"a door to a hall or stairs that lead outside\",\"window\":\"an upper window with an escape ladder\",\"alarm\":\"a smoke alarm inside\",\"ways_out\": ... (653 characters)",
 "people": "[{\"id\":\"P1\",\"name\":\"Priya\",\"room\":\"R1\",\"needs\":\"gets out alone\",\"helper\":\"\"},{\"id\":\"P2\",\"name\":\"Jonah\",\"room\":\"R2\",\"needs\":\"gets out alone\",\"helper\":\"\"}]",
 "household": "{\"alarm_every_level\":\"yes\",\"alarms_last_tested\":\"one to six months ago\",\"last_drill\":\"never\",\"timed_drill_seconds\":\"\",\"meeting_spot\":\"the bus shelter across the street\"}",
 "facts": "[{\"id\":\"F1\",\"level\":\"gap\",\"ref\":\"R1\",\"text\":\"Front bedroom has one way out (the door; an upper window with no ladder).\"},{\"id\":\"F2\",\"level\":\"gap\",\"ref\":\"H\",\"text\":\"The smoke alarms were last tested one to six months ago; the advice is once a month.\"},{\"id\":\"F3\",\"level\":\"gap\",\"ref\":\"H\",\"text\":\"The household has never done a practice drill; the advice is twice a year.\"}]",
 "figures": "{\"blockers\":\"0\",\"gaps\":\"3\",\"rooms\":\"3\",\"sleeping_rooms\":\"2\",\"people\":\"2\"}",
 "notes": "A shared flat at the top of the building. There is one stairwell. Jonah sleeps with earplugs in.",
 "clip_note": ""
}

Reply:

{
 "task": "premortem",
 "verdict": "patch",
 "title": "Three gaps to close, starting with Front bedroom",
 "summary": "This plan needs a patch - there are no blockers, but three gaps could trip you up at night. Make sure Front bedroom has a real second way out, since its window currently has no ladder.",
 "failures": [
  {
   "fact": "F1",
   "ref": "R1",
   "story": "Front bedroom's only other way out is an upper window with no ladder, so Priya may not have a second way out if the door route isn't usable.",
   "severity": "medium",
   "fix": "Get an escape ladder for Front bedroom's window so it becomes a real second way out."
  },
  {
   "fact": "F2",
   "ref": "H",
   "story": "The smoke alarms were last tested one to six months ago, so you can't be sure every one of them would still sound tonight.",
   "severity": "medium",
   "fix": "Test every smoke alarm now, then keep testing monthly going forward."
  },
  {
   "fact": "",
   "ref": "P2",
   "story": "Jonah sleeps with earplugs in, so the smoke alarm might not be loud enough to wake Jonah up.",
   "severity": "medium",
   "fix": "Make sure Jonah can feel or see an alert, not just hear the alarm, since earplugs are worn at night."
  },
  {
   "fact": "F3",
   "ref": "H",
   "story": "The household has never run a practice drill, so no one yet knows this plan well enough to follow it quickly at night.",
   "severity": "medium",
   "fix": "Pick a night soon and walk through the whole plan together, from alarm to meeting spot."
  }
 ],
 "drill": [
  "Whoever notices smoke or hears the alarm first wakes everyone else right away.",
  "Priya leaves Front bedroom by the door, since the window has no ladder.",
  "Jonah leaves Back bedroom by the door, or the window using the escape ladder.",
  "During the drill, check that Jonah notices the alarm despite wearing earplugs.",
  "Everyone meets at the bus shelter across the street once outside.",
  "Time the whole drill from alarm to meeting spot and write it down."
 ],
 "questions": [
  "If the one stairwell can't be used, is there another way down for either bedroom?",
  "How would Jonah be woken if the alarm doesn't carry through the earplugs?"
 ],
 "keep": "Back bedroom already has two ways out, and the household has a meeting spot picked: the bus shelter across the street."
}

a single-storey house with nothing for the page to fix - drill

Input:

{
 "task": "premortem",
 "rooms": "[{\"id\":\"R1\",\"name\":\"Our room\",\"level\":\"ground level\",\"door\":\"a door to a hall or stairs that lead outside\",\"window\":\"a window you can climb out of at ground level\",\"alarm\":\"a smoke alarm inside\",\"ways_out\":2,\"sleepers\":[\"P1\",\"P2\"]},{\"id\":\"R2\",\"name\":\"Nursery\",\"level\":\"ground level\",\"door\":\"a door to a hall or stairs that lead outside\",\"window\":\"a window you can climb out of at ground level\",\"alarm\":\"a smoke alarm ins ... (681 characters)",
 "people": "[{\"id\":\"P1\",\"name\":\"Lena\",\"room\":\"R1\",\"needs\":\"gets out alone\",\"helper\":\"\"},{\"id\":\"P2\",\"name\":\"Omar\",\"room\":\"R1\",\"needs\":\"gets out alone\",\"helper\":\"\"},{\"id\":\"P3\",\"name\":\"Noor\",\"room\":\"R2\",\"needs\":\"needs help to get out (a baby, or a mobility, sight or hearing need)\",\"helper\":\"P1\"}]",
 "household": "{\"alarm_every_level\":\"yes\",\"alarms_last_tested\":\"this month\",\"last_drill\":\"in the last six months\",\"timed_drill_seconds\":\"95\",\"meeting_spot\":\"the big oak by the mailbox\"}",
 "facts": "[]",
 "figures": "{\"blockers\":\"0\",\"gaps\":\"0\",\"rooms\":\"3\",\"sleeping_rooms\":\"2\",\"people\":\"3\"}",
 "notes": "Single-storey house. The baby monitor is on Lena's side of the bed. The back door sticks in wet weather.",
 "clip_note": ""
}

Reply:

{
 "task": "premortem",
 "verdict": "drill",
 "title": "Ready to drill - keep the habits going",
 "summary": "This plan has no blockers and no gaps, so it is ready to practise. Run the drill soon so Lena, Omar, and Noor all know the steps by heart.",
 "failures": [
  {
   "fact": "",
   "ref": "H",
   "story": "If the back door sticks in wet weather, whoever reaches it at night could be delayed trying to force it open instead of getting outside quickly.",
   "severity": "medium",
   "fix": "Check and fix the back door so it opens easily in wet weather, or plan to use a window instead."
  },
  {
   "fact": "",
   "ref": "P1",
   "story": "Because the baby monitor sits only on Lena's side of the bed, if Lena doesn't stir right away at night, no one else nearby may hear Noor.",
   "severity": "medium",
   "fix": "Add a second monitor or alert on Omar's side too, so either of you can hear Noor overnight."
  },
  {
   "fact": "",
   "ref": "H",
   "story": "A plan that depends on alarms tested this month and a recent drill could quietly go stale if the household doesn't keep repeating them.",
   "severity": "low",
   "fix": "Put a reminder on the calendar to re-test alarms monthly and repeat the practice drill every six months."
  }
 ],
 "drill": [
  "Sound the smoke alarm to start the drill, as if it had gone off.",
  "Lena and Omar wake right away in Our room and prepare to leave.",
  "Omar leaves Our room alone, using its door or the window.",
  "Lena goes to the Nursery to wake Noor and help Noor get ready to leave.",
  "Lena helps Noor leave the Nursery, using its door or the window.",
  "Everyone meets at the big oak by the mailbox.",
  "Time the drill and try to beat 95 seconds."
 ],
 "questions": [
  "If Lena is the one who doesn't wake first, who helps Noor out of the Nursery?",
  "If the back door sticks, does everyone know to use a window instead?",
  "Where exactly outside is the big oak by the mailbox, so everyone finds it in the dark?"
 ],
 "keep": "Every bedroom already has two ways out, alarms were tested this month, and Noor already has a named helper, Lena."
}

Build the input yourself

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

const K = WayKit;
const form = K.form({
  rooms: [{ name: "Bedroom", level: "2", door: "yes", window: "high", alarm: "yes" }],
  people: [{ name: "Alex", room: "Bedroom", needs: "none", helper: "" }],
  every_level: "yes", tested: "month", drill: "never", meet: "the corner mailbox", notes: "" });
const r = K.read(form);                            // r.rooms (ways out), r.facts (blockers and gaps), r.fig (counts, verdict), r.clock (rough seconds)
const built = K.buildInput(form);
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.csv(built); K.ics(new Date(), form.meet)     // the room sheet and the reminders calendar

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://two-ways-out.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":"two-ways-out"}' | 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": "premortem", "rooms": "[{\"id\":\"R1\",\"name\":\"Front bedroom\",\"level\":\"two floors up\",\"door\":\"a door to a hall or stairs that lead outside\",\"window\":\"an upper window with no ladder\",\"alarm\":\"a smoke alarm inside\",\"ways_out\":1,\"sleepers\":[\"P1\"]}, ...]", "people": "[{\"id\":\"P1\",\"name\":\"Priya\",\"room\":\"R1\",\"needs\":\"gets out alone\",\"helper\":\"\"},{\"id\":\"P2\",\"name\":\"Jonah\",\"room\":\"R2\",\"needs\":\"gets out alone\",\"helper\":\"\"}]", "household": "{\"alarm_every_level\":\"yes\",\"alarms_last_tested\":\"one to six months ago\",\"last_drill\":\"never\",\"timed_drill_seconds\":\"\",\"meeting_spot\":\"the bus shelter across the street\"}", "facts": "[...]", "figures": "{\"blockers\":\"0\",\"gaps\":\"3\",\"rooms\":\"3\",\"sleeping_rooms\":\"2\",\"people\":\"2\"}", "notes": "A shared flat at the top of the building. There is one stairwell. Jonah sleeps with earplugs in."}
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 two-ways-out:<task>:<inputHash>:a<attempt> (the one task is premortem), 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="two-ways-out:premortem:$(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: two-ways-out:premortem:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json