← They Served
API
Tokens

Drive They Served from your own code

The metered reply is available over HTTP. It takes what a family knows about someone who served - the name, the relationship, the pronoun, where the tribute goes, its length and tone, every fact with the pile the page sorted it into, and the page's counts - and returns one JSON object: a verdict, a title, a line for the plaque, the tribute, a note on how each fact was used, the questions to ask and what to check before sharing.

The division of labour first: the page owns the sorting and the rule; the model owns the words; the page re-checks the reply. The piles, the pronoun and the verdict rule come from servekit.js, which you can load in node to build the input exactly as the page does (see Build the input yourself).

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":"they-served"}), 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 (name, pronouns, words_min, words_max, 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 reply 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 verdict rule

There is one task, so no task field is sent. The verdict is decided by a fixed rule on the page's counts, checked in this order - the first that applies wins:

  1. check_first - nothing is on record (figures.records is "0").
  2. mostly_stories - family stories outnumber the facts on record.
  3. on_record - everything else.

A fact's pile is the page's reading of its wording: a question, or a line that says nobody knows, is unknown; a line where a document says something ("discharge papers say...") is record; a line that says who told it or hedges ("Grandma said", "apparently", "Dad thinks") is story; a line that names papers, a photo, a medal or a letter is record; anything else is treated as story. A tag at the end of a line (", record", ", story", ", unknown") overrides it. The declared input schema carries scalars only, so the array- and object-valued fields are JSON-encoded strings.

Input fields

FieldTypeNotes
name, relationstringWhat the family calls the person (required) and who they are to you.
pronounsstringhe, she, they or name only. Required.
purpose, purpose_labelstringpost, school, program or family, and its plain label.
length, words_min, words_maxstringshort (50-80), standard (100-150) or long (170-230); the bounds as strings. Required.
tonestringplain, warm or formal.
factsstring (JSON)Up to 10 facts, in order: [{fact, source, source_label}]; source is record, story or unknown. Required.
figuresstring (JSON){verdict, records, stories, unknowns, total}, every value a string. Required.
wishesstringYour note (something to leave out, or how it will be read), or empty; past 400 characters the opening and closing are kept with the middle marked [...].
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: verdict (check_first | mostly_stories | on_record), title, plaque (one line, at most 12 words), tribute (between words_min and words_max words, paragraphs separated by a blank line), facts (one {fact, use, how} per input fact, in order, the fact copied exactly; use is stated, framed or left_out), questions (up to 6) and before_sharing. The page re-checks every reply: the verdict against the rule, one note per fact in order, no not-known fact used and no family story stated as plain fact, the tribute's length, every number and year from the facts, no hero, sacrifice or battle words the facts do not use, no added frequencies or feelings, the pronoun, and no internal field names in the prose.

Worked examples

Replies produced against the app's prompt for the four samples (every name and fact is made up; the full replies ship in example-result.js). Together they cover every verdict and every pile.

Grandad - on record, four facts on record, two family stories and one question

Input:

{
 "name": "Grandad Joe",
 "relation": "my grandfather",
 "pronouns": "he",
 "purpose": "post",
 "purpose_label": "a short post for 11 November",
 "length": "standard",
 "words_min": "100",
 "words_max": "150",
 "tone": "warm",
 "facts": "[{\"fact\":\"Discharge papers say he served in the Royal Navy from 1943 to 1946\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"A photo of him in uniform, dated 1944 on the back\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"His service medals are in a tin in Mum's wardrobe\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"His record card lists him as a radio operator\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"Grandma said he never talked about the war, only about the food\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"He used to whistle the same tune while washing up\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Which ship was he on?\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"}]",
 "figures": "{\"verdict\":\"on_record\",\"records\":\"4\",\"stories\":\"2\",\"unknowns\":\"1\",\"total\":\"7\"}",
 "wishes": ""
}

Reply:

{
 "verdict": "on_record",
 "title": "Remembering Grandad Joe",
 "plaque": "Royal Navy radio operator, 1943 to 1946",
 "tribute": "My grandfather, Grandad Joe, served in the Royal Navy from 1943 to 1946, which is what his discharge papers say. There is a photo of him in uniform, dated 1944 on the back. His record card lists him as a radio operator, and his service medals are in a tin in Mum's wardrobe.\n\nAlongside what is on record, the family also keeps stories about him, each one told rather than checked. Grandma said he never talked about the war, only about the food. He also used to whistle the same tune while he was washing up, or so it is remembered.",
 "facts": [
  {
   "fact": "Discharge papers say he served in the Royal Navy from 1943 to 1946",
   "use": "stated",
   "how": "Opens the tribute, stated plainly as the record shows."
  },
  {
   "fact": "A photo of him in uniform, dated 1944 on the back",
   "use": "stated",
   "how": "Stated plainly, placed right after the service dates."
  },
  {
   "fact": "His service medals are in a tin in Mum's wardrobe",
   "use": "stated",
   "how": "Stated plainly alongside the record card detail."
  },
  {
   "fact": "His record card lists him as a radio operator",
   "use": "stated",
   "how": "Stated plainly, giving his role on record."
  },
  {
   "fact": "Grandma said he never talked about the war, only about the food",
   "use": "framed",
   "how": "Framed as what Grandma said, kept as her story."
  },
  {
   "fact": "He used to whistle the same tune while washing up",
   "use": "framed",
   "how": "Framed as a family memory, closing the tribute."
  },
  {
   "fact": "Which ship was he on?",
   "use": "left_out",
   "how": "Not known, so it becomes a question instead."
  }
 ],
 "questions": [
  "Do the discharge papers, or any other paperwork, say which ship he was on?",
  "Does anyone keep a letter or note that might mention the tune he used to whistle?"
 ],
 "before_sharing": "The main facts are on record, so before sharing, check the names and years against the discharge papers, photo, and record card to be sure they all agree."
}

Great-aunt - mostly family stories, short and formal for a service programme

Input:

{
 "name": "Great-Aunt Rosa",
 "relation": "my great-aunt",
 "pronouns": "she",
 "purpose": "program",
 "purpose_label": "a few lines for a service programme or memorial page",
 "length": "short",
 "words_min": "50",
 "words_max": "80",
 "tone": "formal",
 "facts": "[{\"fact\":\"Her obituary in the local paper says she served as an Army nurse\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"The family says she nursed soldiers in England before D-Day\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Mom remembers her saying the night shifts were the hardest part\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"She apparently met her husband at a dance near the base\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Not sure which hospital she was posted to\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"},{\"fact\":\"Did she stay in the service after the war?\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"}]",
 "figures": "{\"verdict\":\"mostly_stories\",\"records\":\"1\",\"stories\":\"3\",\"unknowns\":\"2\",\"total\":\"6\"}",
 "wishes": "It will be read aloud, so keep the sentences short."
}

Reply:

{
 "verdict": "mostly_stories",
 "title": "Remembering Great-Aunt Rosa",
 "plaque": "Served as an Army nurse",
 "tribute": "My great-aunt Rosa's obituary in the local paper says she served as an Army nurse.\n\nThe family says she nursed soldiers in England before D-Day. Mom remembers her saying the night shifts were the hardest part.\n\nAs the story goes, she met her husband at a dance near the base.",
 "facts": [
  {
   "fact": "Her obituary in the local paper says she served as an Army nurse",
   "use": "stated",
   "how": "Used directly as the opening line about her role as a nurse."
  },
  {
   "fact": "The family says she nursed soldiers in England before D-Day",
   "use": "framed",
   "how": "Kept as a family story, worded with 'the family says' in the tribute."
  },
  {
   "fact": "Mom remembers her saying the night shifts were the hardest part",
   "use": "framed",
   "how": "Kept as Mom's memory, word for word, in the second paragraph."
  },
  {
   "fact": "She apparently met her husband at a dance near the base",
   "use": "framed",
   "how": "Kept as a family story, introduced with 'as the story goes' before the detail."
  },
  {
   "fact": "Not sure which hospital she was posted to",
   "use": "left_out",
   "how": "Not known, so turned into one of the questions to ask."
  },
  {
   "fact": "Did she stay in the service after the war?",
   "use": "left_out",
   "how": "Not known, so turned into a question about her later service."
  }
 ],
 "questions": [
  "Could the official service records show which hospital she was posted to?",
  "Do the official service records show whether she stayed in the service after the war?",
  "Is there a letter or paper from her time nursing in England that an older relative might have kept?",
  "Does anyone have a photo from the dance where she met her husband?"
 ],
 "before_sharing": "Most of this rests on family stories, so keep them described as stories rather than as checked facts when the programme is shared."
}

Great-grandpa - check before sharing, nothing on record, for a school assignment

Input:

{
 "name": "Great-Grandpa Walter",
 "relation": "my great-grandfather",
 "pronouns": "he",
 "purpose": "school",
 "purpose_label": "a school assignment about someone who served",
 "length": "standard",
 "words_min": "100",
 "words_max": "150",
 "tone": "plain",
 "facts": "[{\"fact\":\"The family story is that he lied about his age to join up\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Dad thinks he was in the army in France in the First World War\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"He supposedly kept a diary, but nobody knows where it went\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"He came home and worked on the railways for forty years\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"What regiment was he in?\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"},{\"fact\":\"Is there a photo of him in uniform anywhere?\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"}]",
 "figures": "{\"verdict\":\"check_first\",\"records\":\"0\",\"stories\":\"4\",\"unknowns\":\"2\",\"total\":\"6\"}",
 "wishes": ""
}

Reply:

{
 "verdict": "check_first",
 "title": "Remembering Great-Grandpa Walter",
 "plaque": "Remembered by the family",
 "tribute": "My great-grandfather, Great-Grandpa Walter, is known to our family mostly through what is remembered and believed, rather than through papers we can point to. As the family tells it, he lied about his age to join up, and Dad thinks he was in the army in France in the First World War, though nobody has confirmed either part of it. The family also says he kept a diary, but nobody knows where it went, so that part of the story has no paper to back it up either.\n\nWhat the family remembers, too, is this: he came home and worked on the railways for forty years.",
 "facts": [
  {
   "fact": "The family story is that he lied about his age to join up",
   "use": "framed",
   "how": "Used in the tribute as a family story about joining up."
  },
  {
   "fact": "Dad thinks he was in the army in France in the First World War",
   "use": "framed",
   "how": "Used in the tribute as what Dad thinks, not as a certainty."
  },
  {
   "fact": "He supposedly kept a diary, but nobody knows where it went",
   "use": "framed",
   "how": "Used in the tribute as a family story about a lost diary."
  },
  {
   "fact": "He came home and worked on the railways for forty years",
   "use": "framed",
   "how": "Used in the tribute as a family story about his years on the railways."
  },
  {
   "fact": "What regiment was he in?",
   "use": "left_out",
   "how": "Not known, so turned into a question about his regiment."
  },
  {
   "fact": "Is there a photo of him in uniform anywhere?",
   "use": "left_out",
   "how": "Not known, so turned into a question about a photo in uniform."
  }
 ],
 "questions": [
  "Do the official service records show what regiment he was in?",
  "Does anyone in the family have a photo of him in uniform?",
  "Could Dad say more about how he knows Walter served in France in the First World War?",
  "Does anyone know if the diary he is said to have kept still exists, and where?"
 ],
 "before_sharing": "Nothing here is on record yet: check with an older relative or the official service records before sharing this beyond the family, keep the stories described as family memory, and note in the assignment which parts are family stories."
}

A living parent - on record, the name only, one fact left out as asked

Input:

{
 "name": "Sam",
 "relation": "my parent",
 "pronouns": "name only",
 "purpose": "family",
 "purpose_label": "a message for the family",
 "length": "short",
 "words_min": "50",
 "words_max": "80",
 "tone": "warm",
 "facts": "[{\"fact\":\"Sam's discharge certificate shows six years in the Air Force, 1998 to 2004\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"Sam still keeps the old ID badge from the base in a drawer\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"Sam deployed overseas twice\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Sam says the best part was the friends made on base\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"}]",
 "figures": "{\"verdict\":\"on_record\",\"records\":\"2\",\"stories\":\"2\",\"unknowns\":\"0\",\"total\":\"4\"}",
 "wishes": "Sam does not like talking about the deployments - leave them out."
}

Reply:

{
 "verdict": "on_record",
 "title": "A Family Message for Sam",
 "plaque": "Air Force service, 1998 to 2004",
 "tribute": "My parent, Sam, served six years in the Air Force, from 1998 to 2004, as the discharge certificate shows. Sam still keeps the old ID badge from the base in a drawer.\n\nAlong with what is on record, Sam also says that the best part was the friends made on base.",
 "facts": [
  {
   "fact": "Sam's discharge certificate shows six years in the Air Force, 1998 to 2004",
   "use": "stated",
   "how": "Stated plainly, opening the message with the service record."
  },
  {
   "fact": "Sam still keeps the old ID badge from the base in a drawer",
   "use": "stated",
   "how": "Stated plainly right after the service years."
  },
  {
   "fact": "Sam deployed overseas twice",
   "use": "left_out",
   "how": "Left out, as Sam asked, since it is about deployments."
  },
  {
   "fact": "Sam says the best part was the friends made on base",
   "use": "framed",
   "how": "Framed as what Sam says, closing the message."
  }
 ],
 "questions": [
  "Is there a photo or letter from Sam's time on base that shows some of those friends?"
 ],
 "before_sharing": "The main facts are on record, so before sharing, check the years against the discharge certificate, and keep the friends story framed as what Sam says, not as confirmed record."
}

Build the input yourself

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

const K = ServeKit;
const form = K.form({ name: "Grandad Joe", relation: "my grandfather", pronouns: "auto", purpose: "post", length: "standard", tone: "warm",
  facts: "Discharge papers say he served from 1943 to 1946\nGrandma said he never talked about it\nWhich ship was he on?", wishes: "" });
const r = K.read(form);                            // r.facts (each with its pile), r.pronoun, r.fig.verdict
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, reply), K.md(reply, built, form)  // the fact ledger and the Markdown export

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://they-served.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":"they-served"}' | 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 - {"name": "Grandad Joe", "relation": "my grandfather", "pronouns": "he", "purpose": "post", "purpose_label": "a short post for 11 November", "length": "standard", "words_min": "100", "words_max": "150", "tone": "warm", "facts": "[...]", "figures": "{...}", "wishes": ""}
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 input: a retry with the same key never bills twice. The page uses they-served:tribute:<inputHash>:a<attempt> (the one task is tribute), 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="they-served:tribute:$(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: they-served:tribute:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json