← Small Blizzard
API

Drive Small Blizzard from your own code

The one metered task, compose, is available over HTTP. It takes one small ordinary moment in the person's own words and the pieces standing under the glass, and returns the words for the globe: a two-to-six-word plaque built only from the moment's own words, the stretch of the moment it came from, one note per piece and a souvenir-shop tag.

The division of labour first: the page decides the scene; the model writes the words. The page finds the shelf things the moment names (25 figures, each with its everyday words), stands the first four on the snow and the first sky thing above them, picks the snow and runs the particle system. After the reply it checks that every piece came back in order with its label, that the plaque uses only words from the stretch it quotes, that the stretch really is in the moment, and that no day, time or number crept in that the moment never gave.

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":"small-blizzard"}), 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, pieces, piece_count) 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 plaque and notes are metered and need 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: compose (the one task is compose); a missing or different one is still answered as compose. All fields are strings.

FieldRequiredWhat it holds
taskyesAlways compose.
momentyesThe moment in the person's words, up to 400 characters (clipped on a word boundary).
piecesyesNumbered lines N. figure - label, one per piece under the glass (at most four ground pieces and one sky piece), in the order the moment names them. Build it with GlobeKit.buildInput.
piece_countyesHow many lines pieces has, as a string.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The shelf (figure ids): person, pair, cat, dog, tree, plant, house, bench, lamp, chair, window, mug, cake, bread, book, letter, phone, key, umbrella, boot, bicycle, sun (sky), moon (sky), cloud (sky), bird (sky).

The reply

One JSON object, no prose around it:

{"task": "compose", "plaque": "...", "plaque_from": "...", "pieces": [{"n": 1, "label": "coffee", "note": "..."}], "tag": "..."}
FieldWhat it holds
plaqueTwo to six words for the brass plaque, built only from the words of plaque_from plus small linking words.
plaque_fromOne stretch of the moment (3 to 12 words in a row), copied exactly.
pieces[]One entry per input piece, in order: n, label (the input label, unchanged) and note (one sentence using only what the moment says about that thing).
tagOne sentence in the voice of a souvenir-shop label: what the globe keeps.

The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a missing or extra note, a changed label, a plaque of the wrong length or with words not in its quoted stretch, a quoted stretch that is not in the moment, and any day, time or number the moment never gave.

Worked examples - every sample

These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover a sky piece, four pieces on the snow with one thing left on the shelf, a person word and a piece the moment says nothing more about.

Coffee on the back step - "Coffee on the back step"

Input:

{
 "task": "compose",
 "moment": "First coffee on the back step while the cat watched the rain stop.",
 "pieces": "1. mug - coffee\n2. bench - back step\n3. cat - cat\n4. cloud - rain",
 "piece_count": "4"
}

Reply:

{
 "task": "compose",
 "plaque": "Coffee on the back step",
 "plaque_from": "First coffee on the back step while the cat watched the rain",
 "pieces": [
  {
   "n": 1,
   "label": "coffee",
   "note": "Coffee was first, on the back step."
  },
  {
   "n": 2,
   "label": "back step",
   "note": "The back step was under the first coffee."
  },
  {
   "n": 3,
   "label": "cat",
   "note": "The cat watched the rain stop."
  },
  {
   "n": 4,
   "label": "rain",
   "note": "Rain stopped while the cat watched."
  }
 ],
 "tag": "This globe keeps the first coffee on the back step, while the cat watched the rain stop."
}

The bicycle fixed at last - "Fixing the bike with my neighbour"

Input:

{
 "task": "compose",
 "moment": "My neighbour and I finally fixed the old bike, then ate toast on the bench under the lamp until the moon came up.",
 "pieces": "1. person - neighbour\n2. bicycle - bike\n3. bread - toast\n4. bench - bench\n5. moon - moon",
 "piece_count": "5"
}

Reply:

{
 "task": "compose",
 "plaque": "Fixing the bike with my neighbour",
 "plaque_from": "My neighbour and I finally fixed the old bike",
 "pieces": [
  {
   "n": 1,
   "label": "neighbour",
   "note": "You and the neighbour finally fixed the old bike together."
  },
  {
   "n": 2,
   "label": "bike",
   "note": "The old bike was finally fixed by you and the neighbour."
  },
  {
   "n": 3,
   "label": "toast",
   "note": "Toast was eaten on the bench under the lamp."
  },
  {
   "n": 4,
   "label": "bench",
   "note": "Under the lamp, the bench is where toast was eaten until the moon came up."
  },
  {
   "n": 5,
   "label": "moon",
   "note": "The moon came up while toast was being eaten on the bench under the lamp."
  }
 ],
 "tag": "This globe keeps an old bike fixed with a neighbour, toast eaten on a bench under a lamp, and a moon that came up."
}

A letter by the window - "A letter arriving on my birthday"

Input:

{
 "task": "compose",
 "moment": "A letter from my sister arrived on my birthday and I read it twice by the window with a slice of cake.",
 "pieces": "1. letter - letter\n2. person - sister\n3. window - window\n4. cake - cake",
 "piece_count": "4"
}

Reply:

{
 "task": "compose",
 "plaque": "A letter arriving on my birthday",
 "plaque_from": "A letter from my sister arrived on my birthday",
 "pieces": [
  {
   "n": 1,
   "label": "letter",
   "note": "The letter from your sister arrived on your birthday, and you read it twice by the window with a slice of cake."
  },
  {
   "n": 2,
   "label": "sister",
   "note": "The letter that arrived on your birthday came from your sister."
  },
  {
   "n": 3,
   "label": "window",
   "note": "You read the letter twice by the window."
  },
  {
   "n": 4,
   "label": "cake",
   "note": "A slice of cake was with you by the window as you read the letter twice."
  }
 ],
 "tag": "A letter from your sister, arrived on your birthday, read twice by the window with a slice of cake."
}

Build the input yourself

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

const K = GlobeKit, R = ReplyKit;
const built = K.buildInput(K.form({ moment: "Tea on the bench while the dog slept", snow: "auto" }), null);  // built.ok is false, with built.note, until a shelf thing is named
const wire = K.toWire(built.input);   // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes        // the page's re-check of the words

Form keys: moment (up to 400 characters) and snow (auto, flakes, glitter, confetti, petals, leaves or bubbles - the snow is the page's business and is not sent). Pass a list of {figure, label} as the second argument to buildInput to set the pieces by hand.

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://small-blizzard.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":"small-blizzard"}' | 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": "compose", "moment": "...", "pieces": "1. mug - coffee\n2. cat - cat", "piece_count": "2"}
# - 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 small-blizzard:<task>:<inputHash>:a<attempt> (the one task is compose), 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="small-blizzard:compose:$(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: small-blizzard:compose:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json