← Which Door
API

Drive Which Door from your own code

The one metered task, doors, is available over HTTP. It takes a list of up to eight small personal changes, each with five answers - how long undoing it would take, what undoing it would cost, whether it touches anyone else, whether a smaller try-out exists and whether waiting closes a chance - and returns a door read: for every change the door (revolving, swing, stable or trapdoor), the way back, why that door, a try-out where one fits, what waiting costs and one next step, plus a one-sentence headline for the list.

The division of labour first: the page sorts; the model reads. The page hangs each change on a door from its answers in a fixed order and sends the result as facts. The model keeps to those doors unless a change plainly contradicts an answer (a tattoo you said could be undone within a few days), and then says why in differs. After the reply, the page lists anything that does not match. A thinking aid, not professional advice: the read never says whether a change is a good idea.

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": "..." } }

The token is minted for this app (the guest call takes {"slug":"which-door"}), 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, items, facts) 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 door read is metered and needs a personal sign-in.
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, the input and the rules

Every run sends task: doors (the one task is doors); a missing or different one is still answered as doors. All fields are strings.

\n \n
FieldRequiredWhat it holds
taskyesAlways doors.
itemsyesOne change a line: N. the change | undo time: ... | undo cost: ... | affects: ... | smaller try-out: ... | if you wait: ... | note: .... Undo time is within a few days, a few weeks, months or it can't be undone; undo cost next to nothing, some money, effort or awkwardness or a lot; affects only me or someone else too; smaller try-out yes, no or not sure; if you wait nothing or a chance closes; the note is none when empty. Up to eight lines; a change is up to 160 characters, a note up to 240.
factsyesThe page's door for every line with the answers that set it, the count per door and the lines where waiting closes a chance. Build it with DoorKit.buildInput.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The doors, in order: undoing takes months, can't be done, or costs a lot - stable if a smaller try-out exists, otherwise trapdoor; else undoing takes a few weeks, costs some money, effort or awkwardness, or touches someone else - swing; otherwise revolving. A change where waiting closes a chance gets a staged step rather than a delay.

The reply

One JSON object, no prose around it:

{"task": "doors", "headline": "...", "items": [{"n": 1, "door": "stable", "undo_path": "...", "why": "...", "try_first": "...", "if_wait": "...", "next_step": "...", "differs": ""}]}
FieldWhat it holds
headlineOne sentence: how many changes can be decided now and which one, if any, needs slow thought.
items[].nThe line number, one entry per input line, in order.
items[].doorrevolving, swing, stable or trapdoor.
items[].undo_pathThe concrete way back, or what cannot come back.
items[].whyThe answers that set the door.
items[].try_firstThe smaller try-out for a stable door; a stakes-lowering step for a trapdoor; empty otherwise.
items[].if_waitWhat waiting costs.
items[].next_stepOne small action matched to the door, never a date and never which way to choose.
items[].differsEmpty, or why the change contradicts an answer when the read moved it off the page's door.

The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a missing change, an entry for a change that is not on the list, a door moved without a reason, a stable door with no try-out, a next step that sets a day or time you did not give, and headline counts that disagree with the doors.

Worked examples - every door

\n

These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover every door - revolving, swing, stable and trapdoor - a change the read moved because its answers did not fit, and changes where waiting closes a chance.

\n

A weekend list - stable, trapdoor, revolving, swing

\n

Input:

\n
{
 "task": "doors",
 "items": "1. Cut a short fringe | undo time: months | undo cost: some money, effort or awkwardness | affects: only me | smaller try-out: yes | if you wait: nothing | note: I could try a clip-in fringe first.\n2. Sell my old road bike | undo time: it can't be undone | undo cost: some money, effort or awkwardness | affects: only me | smaller try-out: not sure | if you wait: nothing | note: I haven't ridden it since I moved.\n3. Switch my morning tea to green tea | undo time: within a few days | undo cost: next to nothing | affects: only me | smaller try-out: not sure | if you wait: nothing | note: none\n4. Paint the hallway dark green | undo time: a few weeks | undo cost: some money, effort or awkwardness | affects: someone else too | smaller try-out: yes | if you wait: nothing | note: I share the flat with Priya.",
 "facts": "Page doors, decided from the answers in this order: months or never or a lot of cost -> stable if a smaller try-out exists, else trapdoor; weeks or some cost or someone else -> swing; otherwise revolving.\n1: stable - undoing it takes months; undoing it costs some money, effort or awkwardness; you said a smaller try-out exists\n2: trapdoor - it can't be undone; undoing it costs some money, effort or awkwardness; you were not sure a smaller try-out exists\n3: revolving - it can be undone within a few days; undoing it costs next to nothing\n4: swing - undoing it takes a few weeks; undoing it costs some money, effort or awkwardness; it touches someone else\nCount: revolving 1 (item 3); swing 1 (item 4); stable 1 (item 1); trapdoor 1 (item 2)\nWaiting closes a chance: none"
}
\n

Reply:

\n
{
 "task": "doors",
 "headline": "One change is ready to decide right now, and selling the old road bike is the one that deserves slow thought.",
 "items": [
  {
   "n": 1,
   "door": "stable",
   "undo_path": "grow the fringe out",
   "why": "You said cutting it takes months to undo, costs some money, effort or awkwardness, and a smaller try-out exists.",
   "try_first": "Try the clip-in fringe first, as you suggested.",
   "if_wait": "Waiting costs nothing.",
   "next_step": "Try the clip-in fringe before deciding whether to cut it.",
   "differs": ""
  },
  {
   "n": 2,
   "door": "trapdoor",
   "undo_path": "the bike won't come back once it's sold",
   "why": "You said selling it can't be undone, costs some money, effort or awkwardness, and you weren't sure a smaller try-out exists.",
   "try_first": "Set the bike aside instead of listing it for sale yet.",
   "if_wait": "Waiting costs nothing.",
   "next_step": "Pause before listing it for sale.",
   "differs": ""
  },
  {
   "n": 3,
   "door": "revolving",
   "undo_path": "switch back to your usual tea",
   "why": "You said switching it back takes within a few days and costs next to nothing.",
   "try_first": "",
   "if_wait": "Waiting costs nothing.",
   "next_step": "Decide it now, either way.",
   "differs": ""
  },
  {
   "n": 4,
   "door": "swing",
   "undo_path": "repaint the hallway another colour",
   "why": "You said undoing it takes a few weeks, costs some money, effort or awkwardness, and it touches Priya too.",
   "try_first": "",
   "if_wait": "Waiting costs nothing.",
   "next_step": "Decide it now either way, and if you paint it, let how dark the hallway looks once it's done be your sign if it isn't working.",
   "differs": ""
  }
 ]
}
\n

One answer that doesn't fit - stable, revolving, swing

\n

Input:

\n
{
 "task": "doors",
 "items": "1. Get a small fern tattoo on my wrist | undo time: within a few days | undo cost: next to nothing | affects: only me | smaller try-out: yes | if you wait: nothing | note: none\n2. Unsubscribe from the cooking newsletter | undo time: within a few days | undo cost: next to nothing | affects: only me | smaller try-out: not sure | if you wait: nothing | note: none\n3. Join the pub quiz team | undo time: a few weeks | undo cost: some money, effort or awkwardness | affects: someone else too | smaller try-out: no | if you wait: a chance closes | note: They need a fourth player and will ask someone else if I don't say.",
 "facts": "Page doors, decided from the answers in this order: months or never or a lot of cost -> stable if a smaller try-out exists, else trapdoor; weeks or some cost or someone else -> swing; otherwise revolving.\n1: revolving - it can be undone within a few days; undoing it costs next to nothing\n2: revolving - it can be undone within a few days; undoing it costs next to nothing\n3: swing - undoing it takes a few weeks; undoing it costs some money, effort or awkwardness; it touches someone else\nCount: revolving 2 (items 1, 2); swing 1 (item 3); stable 0; trapdoor 0\nWaiting closes a chance: item 3"
}
\n

Reply:

\n
{
 "task": "doors",
 "headline": "One of these you can decide right now, and none needs slow thought.",
 "items": [
  {
   "n": 1,
   "door": "stable",
   "undo_path": "Getting the tattoo removed, which takes real time and money.",
   "why": "Getting a tattoo is something that's genuinely hard to undo, even though it's only your own decision to make.",
   "try_first": "Try a temporary fern design on your wrist first, before getting the permanent tattoo.",
   "if_wait": "Waiting costs nothing here.",
   "next_step": "Try the temporary version before deciding on the permanent tattoo.",
   "differs": "You said undoing it would take a few days and cost next to nothing, but a tattoo is hard to fully undo and takes real time and money instead."
  },
  {
   "n": 2,
   "door": "revolving",
   "undo_path": "Resubscribing to the newsletter.",
   "why": "It can be undone within a few days at next to no cost, and it only affects you.",
   "try_first": "",
   "if_wait": "Waiting costs nothing here.",
   "next_step": "Decide about the newsletter now, either way.",
   "differs": ""
  },
  {
   "n": 3,
   "door": "swing",
   "undo_path": "Leaving the quiz team later.",
   "why": "Leaving the team later would take a few weeks and some money, effort or awkwardness, and it touches someone else too.",
   "try_first": "",
   "if_wait": "If you don't say, they'll ask someone else to be the fourth player, and a staged step beats putting it off.",
   "next_step": "Decide about joining the team now, either way, and if you join, notice if you start missing quizzes as a sign it's not working.",
   "differs": ""
  }
 ]
}
\n

Two bigger ones - trapdoor, stable, swing, revolving

\n

Input:

\n
{
 "task": "doors",
 "items": "1. Sell the piano nobody plays | undo time: it can't be undone | undo cost: a lot | affects: someone else too | smaller try-out: no | if you wait: a chance closes | note: My partner learned on it. A neighbour offered to buy it.\n2. Ask to work four days a week | undo time: months | undo cost: a lot | affects: someone else too | smaller try-out: yes | if you wait: a chance closes | note: My manager mentioned a trial period is possible while the new rota is being set.\n3. Turn the sofa to face the window | undo time: within a few days | undo cost: next to nothing | affects: someone else too | smaller try-out: not sure | if you wait: nothing | note: none\n4. Stop buying a coffee on the way to work | undo time: within a few days | undo cost: next to nothing | affects: only me | smaller try-out: not sure | if you wait: nothing | note: none",
 "facts": "Page doors, decided from the answers in this order: months or never or a lot of cost -> stable if a smaller try-out exists, else trapdoor; weeks or some cost or someone else -> swing; otherwise revolving.\n1: trapdoor - it can't be undone; undoing it costs a lot; it touches someone else; you said no smaller try-out exists\n2: stable - undoing it takes months; undoing it costs a lot; it touches someone else; you said a smaller try-out exists\n3: swing - it can be undone within a few days; undoing it costs next to nothing; it touches someone else\n4: revolving - it can be undone within a few days; undoing it costs next to nothing\nCount: revolving 1 (item 4); swing 1 (item 3); stable 1 (item 2); trapdoor 1 (item 1)\nWaiting closes a chance: item 1, item 2"
}
\n

Reply:

\n
{
 "task": "doors",
 "headline": "One change can be decided right now, while selling the piano is the one that needs slow, careful thought.",
 "items": [
  {
   "n": 1,
   "door": "trapdoor",
   "undo_path": "The piano can't come back once it's sold.",
   "why": "It can't be undone, would cost a lot, touches your partner too, and no smaller try-out exists for it.",
   "try_first": "Talk to your partner about it before you answer the neighbour.",
   "if_wait": "Waiting risks losing the neighbour's offer to buy it, though a staged step beats putting this off entirely.",
   "next_step": "Sit with the decision a little longer before telling the neighbour yes or no.",
   "differs": ""
  },
  {
   "n": 2,
   "door": "stable",
   "undo_path": "ask to return to your previous hours",
   "why": "It would take months and cost a lot to undo, and it touches someone else, but a smaller try-out exists.",
   "try_first": "Ask your manager about the trial period while the new rota is being set.",
   "if_wait": "Waiting might mean missing the trial period while the new rota is being set, though a staged step beats putting this off entirely.",
   "next_step": "Do the trial period before asking for the full change.",
   "differs": ""
  },
  {
   "n": 3,
   "door": "swing",
   "undo_path": "turn the sofa back",
   "why": "It can be undone within a few days at next to no cost, but it touches someone else too.",
   "try_first": "",
   "if_wait": "Waiting costs nothing here.",
   "next_step": "Decide now either way, and if you turn it, notice whether the sofa stays facing the window or ends up turned back again.",
   "differs": ""
  },
  {
   "n": 4,
   "door": "revolving",
   "undo_path": "start buying coffee again",
   "why": "It can be undone within a few days at next to no cost, and it only affects you.",
   "try_first": "",
   "if_wait": "Waiting costs nothing here.",
   "next_step": "Decide now, either way.",
   "differs": ""
  }
 ]
}
\n

Build the input yourself

Load the engine in node (global.window = global; require("./doorkit.js"); require("./replykit.js")), then:

const K = DoorKit, R = ReplyKit;
const built = K.buildInput(K.form({ name: "Things I keep putting off", items: [
  { change: "Cut a short fringe", time: "months", cost: "some", others: "me", trial: "yes", wait: "nothing", note: "I could try a clip-in fringe first." },
  { change: "Switch my morning tea to green tea", time: "days", cost: "none" } ] }));  // built.ok is false, with built.note, until every change has time and cost
const wire = K.toWire(built.input);   // POST this to /estimate and /run; built.analysis.rows holds each door
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes        // the page's re-check of the reply
// R.md(built, reply, K.title(built.form))   // the list, the doors and the read as Markdown

Item keys: change, time (days, weeks, months, never), cost (none, some, lots), others (me, others), trial (yes, no, unsure), wait (nothing, closes) and note.

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://which-door.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":"which-door"}' | 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": "doors", "items": "1. Cut a short fringe | undo time: months | ...", "facts": "...", ...}
# - see the field table
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 which-door:<task>:<inputHash>:a<attempt> (the one task is doors), 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="which-door:doors:$(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: which-door:doors:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json