← If They Say
Map API
Tokens

Drive If They Say from your own code

The one metered task, map, is available over HTTP. It takes a small real-life ask - the kind of moment, who you are asking, the ask in your words, the reply you dread, what you would settle for and how you like to sound - plus the page's own read of the ask, and returns a dialogue tree: a one-line ask, three openers, three to six replies from them with branching choices, three or four endings (a yes, a half-yes and a kind no), the dreaded reply marked, and three delivery tips.

The division of labour first: the page owns the read of the ask and the verdict, the tree checks and the route count; the model owns the lines; the page re-checks the reply. All of it comes from ifkit.js, which you can run yourself (see Build the input yourself). The other person's lines are guesses for practice, never predictions.

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":"if-they-say"}), 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, moment, who, ask, voice, page_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: writing a map 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 page's read

Every run sends task: map; a missing or different one is still answered as map. The page reads the ask with plain word rules (IfKit.signals): its word count, vague words (stuff, things, something, help me out, sort it out ...), money (a sum, lend, loan, pay for ...), something ongoing (every week, always, from now on, for good ...) and two asks stacked (and also, as well as ...).

The verdict follows a fixed order, first match wins (IfKit.verdictFor): sharpen if the ask is under four words or has a vague word; shrink if it has a money, ongoing or stacked signal; say_it otherwise. The page sends it in page_facts; the model copies it and writes the ask line to match (blanks in square brackets for sharpen, a smaller first ask without the signal phrases for shrink), and the page flags a reply that does not.

The input

Every value is a string (objects are never sent). app.js builds it with IfKit.buildInput(form).input.

FieldRequiredWhat it holds
taskyesAlways map.
momentyesThe kind of moment, e.g. ask a small favour (one of seven).
whoyesWho you are asking, in your words (at most 120 characters, the middle cut with [...]).
askyesThe ask in your words (at most 300 characters).
worrynoThe reply you dread (at most 240 characters), or none given.
fallbacknoWhat you would settle for (at most 200 characters), or none given.
voiceyeswarm, brief or playful.
notenoAnything else (at most 400 characters), or none given.
page_factsyesThe page's lines (IfKit.pageFacts): verdict, why, ask words, vague words, big-ask signals, worry, fallback and tree size.
clip_notenoWhat the page shortened, or absent.
retry_notenoSent only on the page's one reformat retry.

The reply

The reply is one JSON object in job.output.output. The page parses it with IfKit.parseReply and IfKit.normalize (a cut-off stream is closed by IfKit.closeJson and shown as "N of 9 sections recovered").

{"task": "map", "verdict": "say_it | sharpen | shrink", "verdict_reason": "...", "headline": "...", "ask_line": "...",
 "openers": [{"tone": "warm | direct | playful", "say": "...", "next": "n1"}],
 "nodes": [{"id": "n1", "they": "...", "choices": [{"tone": "warm | direct | playful | exit", "say": "...", "next": "n2 | e1"}]}],
 "worry_node": "n2",
 "endings": [{"id": "e1", "kind": "yes | partial | no", "title": "...", "line": "..."}],
 "tips": ["...", "...", "..."]}
FieldRule
ask_lineOne sentence of at most 25 words, spoken to them: blanks in square brackets for sharpen, a smaller first ask for shrink.
openersExactly 3 - one warm, one direct, one playful - each leading to a line from them.
nodes3 to 6, ids n1, n2 ... in order; they is direct speech of at most 25 words; 2 or 3 choices with different tones; each next is a later node or an ending; an exit always ends the scene.
worry_nodeThe node that voices the reply you dread (or the firmest no when none was given).
endings3 or 4, ids e1 ...; kinds yes, partial and no each at least once; title at most 6 words, line at most 25.
tipsExactly 3, at most 18 words each: when to ask, how to say it, how to leave after a no.
headlineAt most 10 words.

The page then checks the reply against its own facts (IfKit.check, built on IfKit.validate): the verdict, links that lead nowhere or loop back, lines and endings that can never be reached, repeated tones, exits that do not end the scene, a missing kind of ending, an unmarked dreaded reply, an ask line that kept the big-ask phrases or has no blanks when it should, lines from them written as narration, a number or a he/she the brief never gave, and internal labels in the wording. It also counts the routes through the tree (IfKit.routes).

Worked examples, one per verdict

These are the four free samples on the page: the exact input and a stand-in model's reply, unedited.

A seat swap on a train - say_it

Input:

{
 "task": "map",
 "moment": "ask to swap a seat, a slot or a spot",
 "who": "the person in the window seat on my train",
 "ask": "Would you swap seats with me so I can sit next to my sister?",
 "worry": "They say no and I have to sit there feeling awkward for the whole trip.",
 "fallback": "none given",
 "voice": "warm",
 "note": "none given",
 "page_facts": "verdict: say_it\nwhy: the ask is 14 words with no vague words, no money, nothing ongoing and one thing asked\nask words: 14\nvague words: none\nbig-ask signals: none\nworry: given\nfallback: none given\ntree size: 3-6 lines from them, 3-4 endings, 2-3 choices per line"
}

Reply:

{
 "task": "map",
 "verdict": "say_it",
 "verdict_reason": "Your ask is short and clear, asks for just one thing, and does not involve money or anything ongoing, so you can simply say it.",
 "headline": "Ask to swap seats so you can sit together",
 "ask_line": "Would you mind swapping seats with me so I can sit next to my sister?",
 "openers": [
  {
   "tone": "warm",
   "say": "Hi, would you mind swapping seats with me so I can sit next to my sister?",
   "next": "n1"
  },
  {
   "tone": "direct",
   "say": "Would you swap seats with me so I can sit next to my sister?",
   "next": "n1"
  },
  {
   "tone": "playful",
   "say": "Any chance you'd swap seats with me so I can sit next to my sister?",
   "next": "n1"
  }
 ],
 "nodes": [
  {
   "id": "n1",
   "they": "Oh, I'm not sure — I like having the window seat.",
   "choices": [
    {
     "tone": "warm",
     "say": "No pressure, even just for a little while would help.",
     "next": "n2"
    },
    {
     "tone": "direct",
     "say": "Would you be able to swap for the whole ride?",
     "next": "n3"
    },
    {
     "tone": "playful",
     "say": "I promise I'll trade back if you miss the view!",
     "next": "n2"
    }
   ]
  },
  {
   "id": "n2",
   "they": "Alright, I could manage that for a little while.",
   "choices": [
    {
     "tone": "warm",
     "say": "Thank you, I appreciate it.",
     "next": "e2"
    },
    {
     "tone": "direct",
     "say": "Could you manage that for the rest of the trip instead?",
     "next": "n4"
    }
   ]
  },
  {
   "id": "n3",
   "they": "No, I'd rather keep this seat for the trip.",
   "choices": [
    {
     "tone": "warm",
     "say": "That's completely understandable, thank you for thinking about it.",
     "next": "e3"
    },
    {
     "tone": "exit",
     "say": "No problem, I'll stay here then.",
     "next": "e3"
    }
   ]
  },
  {
   "id": "n4",
   "they": "Alright, I don't mind swapping for the whole ride.",
   "choices": [
    {
     "tone": "warm",
     "say": "Thank you so much, I appreciate it.",
     "next": "e1"
    },
    {
     "tone": "direct",
     "say": "Great, thank you — I'll swap with you now.",
     "next": "e1"
    }
   ]
  }
 ],
 "worry_node": "n3",
 "endings": [
  {
   "id": "e1",
   "kind": "yes",
   "title": "They agree to swap",
   "line": "They swap seats with you, and you sit next to your sister."
  },
  {
   "id": "e2",
   "kind": "partial",
   "title": "They offer a shorter swap",
   "line": "They agree to swap with you for a little while, so you can sit next to your sister before moving back."
  },
  {
   "id": "e3",
   "kind": "no",
   "title": "They keep their seat",
   "line": "They keep their seat, you thank them, and you settle into the trip without the swap."
  }
 ],
 "tips": [
  "Ask soon, while it still feels easy for you to bring it up.",
  "Speak slowly, pause after you ask, and look at them while you wait.",
  "If they say no, thank them and settle back into your seat without pushing."
 ]
}

A neighbour's forgotten name - say_it (with a fallback)

Input:

{
 "task": "map",
 "moment": "admit you forgot their name",
 "who": "a neighbour I chat with in the hallway",
 "ask": "Ask them to tell me their name again without making it weird",
 "worry": "They are hurt that I forgot it.",
 "fallback": "Get them to say it to someone else while I listen.",
 "voice": "playful",
 "note": "We have chatted for about a year.",
 "page_facts": "verdict: say_it\nwhy: the ask is 12 words with no vague words, no money, nothing ongoing and one thing asked\nask words: 12\nvague words: none\nbig-ask signals: none\nworry: given\nfallback: given\ntree size: 3-6 lines from them, 3-4 endings, 2-3 choices per line"
}

Reply:

{
 "task": "map",
 "verdict": "say_it",
 "verdict_reason": "Your ask is short and clear, asks for just one specific thing, and doesn't involve money or anything ongoing, so it's ready to say as is.",
 "headline": "Ask your neighbour to tell you their name again",
 "ask_line": "Okay, I have to admit I've blanked on your name - what is it again?",
 "openers": [
  {
   "tone": "warm",
   "say": "Hi - this is a little embarrassing, but I've forgotten your name. Could you tell me again?",
   "next": "n1"
  },
  {
   "tone": "direct",
   "say": "Quick thing - I've blanked on your name. What is it?",
   "next": "n1"
  },
  {
   "tone": "playful",
   "say": "Okay, confession time: I have zero memory of your name - mind telling me again?",
   "next": "n1"
  }
 ],
 "nodes": [
  {
   "id": "n1",
   "they": "Wait, you're telling me you forgot already?",
   "choices": [
    {
     "tone": "warm",
     "say": "I know, my bad - I just want to ask you properly now.",
     "next": "n2"
    },
    {
     "tone": "playful",
     "say": "Guilty as charged! Help a forgetful neighbour out?",
     "next": "n2"
    },
    {
     "tone": "direct",
     "say": "Yeah, I forgot - mind telling me?",
     "next": "n3"
    }
   ]
  },
  {
   "id": "n2",
   "they": "Honestly, it stings a bit that you forgot.",
   "choices": [
    {
     "tone": "warm",
     "say": "I understand, and I really am sorry - I'd still like to know it.",
     "next": "n4"
    },
    {
     "tone": "exit",
     "say": "I hear you - let's just leave it for now.",
     "next": "e3"
    }
   ]
  },
  {
   "id": "n3",
   "they": "I'd rather not just hand it over - maybe another time.",
   "choices": [
    {
     "tone": "warm",
     "say": "That's completely fair - thanks for being honest.",
     "next": "e3"
    },
    {
     "tone": "exit",
     "say": "No worries at all, forget I asked.",
     "next": "e3"
    }
   ]
  },
  {
   "id": "n4",
   "they": "Alright, I suppose it's not that big a deal.",
   "choices": [
    {
     "tone": "warm",
     "say": "Thanks, I appreciate it - so, what is it?",
     "next": "e1"
    },
    {
     "tone": "direct",
     "say": "Or if it's easier, you could mention it to someone nearby and I'll catch it.",
     "next": "e2"
    },
    {
     "tone": "exit",
     "say": "Actually, don't worry about it - we're fine either way.",
     "next": "e3"
    }
   ]
  }
 ],
 "worry_node": "n2",
 "endings": [
  {
   "id": "e1",
   "kind": "yes",
   "title": "They tell you their name",
   "line": "They tell you their name right away, and the conversation carries on easily."
  },
  {
   "id": "e2",
   "kind": "partial",
   "title": "They tell someone else instead",
   "line": "They mention their name to someone nearby, so you catch it without having to ask directly again."
  },
  {
   "id": "e3",
   "kind": "no",
   "title": "You let it go",
   "line": "You decide not to push it, and you both leave the moment on a friendly note."
  }
 ],
 "tips": [
  "Ask when you're next just chatting in the hallway, not in a rushed or crowded moment.",
  "Say it lightly, keep easy eye contact, and pause after asking so it feels playful.",
  "If they'd rather not say, drop it warmly and keep the hallway chats easy."
 ]
}

A vague favour at work - sharpen

Input:

{
 "task": "map",
 "moment": "ask a small favour",
 "who": "my coworker who sits next to me",
 "ask": "Could you maybe help me out with some stuff at work?",
 "worry": "They think I am trying to dump my work on them.",
 "fallback": "none given",
 "voice": "brief",
 "note": "none given",
 "page_facts": "verdict: sharpen\nwhy: the ask leans on vague words (\"help me out\", \"stuff\")\nask words: 11\nvague words: help me out, stuff\nbig-ask signals: none\nworry: given\nfallback: none given\ntree size: 3-6 lines from them, 3-4 endings, 2-3 choices per line"
}

Reply:

{
 "task": "map",
 "verdict": "sharpen",
 "verdict_reason": "Your ask leans on vague words like \"help me out\" and \"stuff\", so it helps to spell out exactly what you need.",
 "headline": "Turn your vague ask into a clear one",
 "ask_line": "Could you help me with [the task] for [how long]?",
 "openers": [
  {
   "tone": "warm",
   "say": "Hey, could you help me with [the task] for [how long]?",
   "next": "n1"
  },
  {
   "tone": "direct",
   "say": "Can you help me with [the task] for [how long]?",
   "next": "n2"
  },
  {
   "tone": "playful",
   "say": "Quick one - any chance you can help me with [the task] for [how long]?",
   "next": "n1"
  }
 ],
 "nodes": [
  {
   "id": "n1",
   "they": "What would you need, and for how long?",
   "choices": [
    {
     "tone": "warm",
     "say": "It's [the task], just for [how long], if that works.",
     "next": "n4"
    },
    {
     "tone": "direct",
     "say": "I need help with [the task] for [how long].",
     "next": "n2"
    },
    {
     "tone": "exit",
     "say": "Never mind, I'll sort it out myself.",
     "next": "e3"
    }
   ]
  },
  {
   "id": "n2",
   "they": "I don't want to end up doing your work for you.",
   "choices": [
    {
     "tone": "warm",
     "say": "I just need a hand, not for you to take it over.",
     "next": "n4"
    },
    {
     "tone": "direct",
     "say": "It's only [the task], not ongoing work.",
     "next": "n3"
    },
    {
     "tone": "exit",
     "say": "That's okay, forget I asked.",
     "next": "e3"
    }
   ]
  },
  {
   "id": "n3",
   "they": "I don't think I can take this on right now.",
   "choices": [
    {
     "tone": "warm",
     "say": "No worries, thanks anyway.",
     "next": "e3"
    },
    {
     "tone": "exit",
     "say": "That's alright, I understand.",
     "next": "e3"
    }
   ]
  },
  {
   "id": "n4",
   "they": "I could probably help with that for a bit.",
   "choices": [
    {
     "tone": "warm",
     "say": "Thanks, that would really help.",
     "next": "e1"
    },
    {
     "tone": "direct",
     "say": "Even a little help would be fine.",
     "next": "e2"
    }
   ]
  }
 ],
 "worry_node": "n2",
 "endings": [
  {
   "id": "e1",
   "kind": "yes",
   "title": "They agree to help",
   "line": "They agree to help, and you thank them and settle what you need."
  },
  {
   "id": "e2",
   "kind": "partial",
   "title": "They offer a little help",
   "line": "They offer to help a bit, and you accept it gladly and thank them."
  },
  {
   "id": "e3",
   "kind": "no",
   "title": "They say no kindly",
   "line": "They say they can't help this time, and you thank them and let it go."
  }
 ],
 "tips": [
  "Ask when you're both free for a moment, not in the middle of something else.",
  "Speak slowly, pause after you ask, and keep steady eye contact.",
  "When they say no, thank them and move on - you don't need to push."
 ]
}

An ongoing ask of a neighbour - shrink

Input:

{
 "task": "map",
 "moment": "ask a small favour",
 "who": "my next-door neighbour",
 "ask": "Could you take my bins out every week from now on while I work nights?",
 "worry": "They say yes to be polite and then resent it.",
 "fallback": "Just this week, while I see how the night shifts go.",
 "voice": "warm",
 "note": "none given",
 "page_facts": "verdict: shrink\nwhy: the ask involves an ongoing ask (\"every week\", \"from now on\")\nask words: 15\nvague words: none\nbig-ask signals: every week, from now on\nworry: given\nfallback: given\ntree size: 3-6 lines from them, 3-4 endings, 2-3 choices per line"
}

Reply:

{
 "task": "map",
 "verdict": "shrink",
 "verdict_reason": "This was shrunk because you asked for an ongoing commitment, every week, from now on, rather than a one-off favour.",
 "headline": "A smaller ask about taking the bins out",
 "ask_line": "Would you mind taking my bins out just this week, while I see how the night shifts go?",
 "openers": [
  {
   "tone": "warm",
   "say": "Hi! Would you mind taking my bins out just this week, while I see how the night shifts go?",
   "next": "n1"
  },
  {
   "tone": "direct",
   "say": "Could you take my bins out just this week, while I see how the night shifts go?",
   "next": "n1"
  },
  {
   "tone": "playful",
   "say": "Quick favour - could you take my bins out just this week, while I see how the night shifts go?",
   "next": "n4"
  }
 ],
 "nodes": [
  {
   "id": "n1",
   "they": "Just this week? I'd need to think about it.",
   "choices": [
    {
     "tone": "warm",
     "say": "No pressure, just let me know whenever you can.",
     "next": "n2"
    },
    {
     "tone": "direct",
     "say": "Does that mean yes or no?",
     "next": "n3"
    },
    {
     "tone": "exit",
     "say": "That's okay, I'll figure something else out.",
     "next": "e3"
    }
   ]
  },
  {
   "id": "n2",
   "they": "Sure, I suppose I could do that.",
   "choices": [
    {
     "tone": "warm",
     "say": "Thank you.",
     "next": "e1"
    },
    {
     "tone": "exit",
     "say": "Actually, don't worry about it, I'll manage.",
     "next": "e3"
    }
   ]
  },
  {
   "id": "n3",
   "they": "I don't think I can manage that, sorry.",
   "choices": [
    {
     "tone": "warm",
     "say": "That's okay, thank you for considering it.",
     "next": "e3"
    },
    {
     "tone": "exit",
     "say": "No problem, thanks anyway.",
     "next": "e3"
    }
   ]
  },
  {
   "id": "n4",
   "they": "I could manage it just once, if that would help.",
   "choices": [
    {
     "tone": "warm",
     "say": "That would still help, thank you.",
     "next": "e2"
    },
    {
     "tone": "exit",
     "say": "That's okay, don't worry about it.",
     "next": "e3"
    }
   ]
  }
 ],
 "worry_node": "n2",
 "endings": [
  {
   "id": "e1",
   "kind": "yes",
   "title": "A kind yes for this week",
   "line": "They agree to take the bins out this week, while you see how the night shifts go."
  },
  {
   "id": "e2",
   "kind": "partial",
   "title": "Just once, not every time",
   "line": "They agree to help just once this week, not every time."
  },
  {
   "id": "e3",
   "kind": "no",
   "title": "A kind no for now",
   "line": "They don't take on the bins, and you leave it there."
  }
 ],
 "tips": [
  "Ask at a calm moment, not when either of you is rushing off.",
  "Speak slowly, pause after you ask, and look at them while you wait.",
  "If they say no, thank them warmly and drop the subject right away."
 ]
}

Build the input yourself

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

const K = IfKit;
const form = K.form({ moment: "sendback", who: "the server at the cafe", ask: "Could you warm my soup up a little more?",
  worry: "They look annoyed", fallback: "", voice: "brief", note: "" });
K.signals(form.ask);                             // {words: 9, vague: [], money: [], ongoing: [], stacked: []}
K.verdictFor(form).verdict;                      // "say_it" - the page's rule, first match wins
K.sketch(form);                                  // the free tree: 3 openers, 3 lines from them, 3 endings
K.validate(K.sketch(form));                      // {problems: [], routes: 8, longest: 2}
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, form).notes             // the page's re-check of the tree
// K.md(form, reply)                             // every branch as a Markdown outline
// K.card(form, reply)                           // the pocket card

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://if-they-say.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":"if-they-say"}' | 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": "map", "moment": "ask to swap a seat, a slot or a spot", "who": "the person in the window seat on my train", "ask": "Would you swap seats with me so I can sit next to my sister?", "worry": "They say no and I have to sit there feeling awkward for the whole trip.", "fallback": "none given", "voice": "warm", "note": "none given", "page_facts": "verdict: say_it\nwhy: the ask is 14 words with no vague words, no money, nothing ongoing and one thing asked\nask words: 14\nvague words: none\nbig-ask signals: none\nworry: given\nfallback: none given\ntree size: 3-6 lines from them, 3-4 endings, 2-3 choices per line"}
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 if-they-say:<task>:<inputHash>:a<attempt> (the one task is map), 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="if-they-say:map:$(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: if-they-say:map:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json