← Creaky Porch
API
Tokens

Drive Creaky Porch from your own code

Both metered lanes are available over HTTP. score takes a door brief - the scene, the youngest guest, how scary, where it plays, the loop length and a leave-out list - and returns a verdict, gentle, spooky or fright, with a mix: two to four looping beds and a spread of timed cues, each with a level and a pan, plus placement tips. check takes the same brief plus a mix and returns keep, adjust or rework, with the issues and the fixed mix. A mix plays in any copy of the page (the share link carries it), so a natural use is a script that scores a porch for each party on a calendar and posts the link.

The division of labour first: the page owns the voices, the level and the limits; the model writes the mix; the page re-checks the reply. The level, the leave-out matching and the findings come from porchkit.js, which runs in node as well as in the browser, and the sound itself comes from engine.js (Web Audio only). PorchKit.buildInput matches the leave-out list to voices and, for check, measures the mix (page_findings) before anything is sent. PorchKit.check holds each reply to the brief: the verdict matches the level the crowd allows, no voice on the leave-out list or above the level got in, no layer is over its cap, cues are 6 seconds apart and within the range for the loop, a fixed mix passes the same checks, and no number or pronoun turns up that the brief does not hold. The scare ratings and caps are this page's own rules of thumb.

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":"creaky-porch"}), 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 such as task, scene, youngest or scare 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: both lanes are metered and need 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, the voices and the verdicts

Every run sends task: score or check; a missing or unknown task is treated as score. The synthesiser has sixteen voices, each with a scare rating from 0 to 3. Beds loop under everything: crickets 0, rain 0, cauldron 0, wind 1, drone 2, heartbeat 2. Cues are one-shots at a second in the loop: chime 0, owl 1, crow 1, creak 2, knock 2, bell 2, howl 2, thunder 3, chains 3, whisper 3. The level is the lower of scare and the cap for the youngest guest (under 5 caps at 1, 5 to 9 at 2, older at 3); a mix may use only voices rated at or below it, with every layer at most 55, 75 or 90 for levels 1, 2 and 3.

LaneOrderVerdictWhen
score-gentleThe level is 1. Only voices rated 0 or 1, levels up to 55.
score-spookyThe level is 2. Voices rated 0 to 2, levels up to 75.
score-frightThe level is 3. Any voice not left out, levels up to 90, at least one voice rated 3.
check1reworkThe mix uses a voice on the leave-out list (avoided), a voice above the level (too-scary), or has no bed (no-bed). The reply carries a new mix that follows every rule.
check2adjustOtherwise, a layer over the cap (too-loud), two cues under 6 seconds apart (crowded), too many beds or cues (busy), too few cues (sparse), or a voice that contradicts the scene (clash). The reply carries the same mix with only those fixes.
check3keepNone of the above. No issues, an empty mix.

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

Input fields

FieldTypeNotes
taskstringscore or check. Required.
scenestringWhat the guests walk past, up to 600 characters (longer scenes lose the middle, marked). Required.
youngeststringunder-5, 5-9, 10-13 or teens-adults. Required.
scarestring"1" cozy, "2" spooky or "3" full fright. Required.
placestringdoorstep, yard or indoors.
loop_minstring"2", "4" or "8": the loop length in minutes. A mix has between that many cues and three times as many (at most 16).
avoidstring (JSON)Up to 8 leave-out lines as typed, e.g. ["no thunder, our dog hides under the bed"]; may be [].
blockedstring (JSON)The voice ids the page matched to those lines (a line saying "sudden" blocks knock, thunder and chains).
mixstring (JSON)check only: {"beds":[{voice, level, pan}], "events":[{at, voice, level, pan}]} - level 0-100, pan -1 to 1, at in whole seconds.
page_findingsstring (JSON)check only: [{rule, voice, at?}], the problems the page measured in that mix.
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. Both lanes carry task, verdict, title and summary. score adds beds ([{voice, level, pan, why}]), events ([{at, voice, level, pan}], ascending) and tips. check adds issues ([{rule, voice, note}]), mix (the fixed mix, empty for keep) and hears (what the youngest guest hears at the door). Parse with PorchKit.parseReply, then normalize(reply, task, loop_min), then check(reply, form), which returns the page's notes. Load the mix into the page with a share link: PorchKit.shareHash(form).

Worked examples

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

score - toddlers at a townhouse step, spooky asked but youngest under 5: gentle

Input:

{
 "task": "score",
 "scene": "Front step of our townhouse: one lit pumpkin, a straw bale and a bowl of candy on a stool. The playgroup toddlers come by first, around dusk, with their parents.",
 "youngest": "under-5",
 "scare": "2",
 "place": "doorstep",
 "loop_min": "2",
 "avoid": "[\"nothing sudden - the baby next door naps late\"]",
 "blocked": "[\"knock\",\"thunder\",\"chains\"]"
}

Reply:

{
 "task": "score",
 "verdict": "gentle",
 "title": "Gentle Dusk for the Toddlers",
 "summary": "A soft hum of crickets and a light breeze carry this doorstep mix, while gentle wind chimes, an owl and a crow are spread through the loop. The mood stays calm and playful, matching the lit pumpkin, the straw bale and the bowl of candy. Playgroup toddlers arrive first at dusk, so every layer stays quiet and unhurried.",
 "beds": [
  {
   "voice": "crickets",
   "level": 35,
   "pan": -0.3,
   "why": "a calm hum for the dusk arrival"
  },
  {
   "voice": "wind",
   "level": 25,
   "pan": 0.4,
   "why": "a light breath of air to match the straw bale"
  }
 ],
 "events": [
  {
   "at": 15,
   "voice": "chime",
   "level": 20,
   "pan": 0.6
  },
  {
   "at": 45,
   "voice": "owl",
   "level": 30,
   "pan": -0.5
  },
  {
   "at": 80,
   "voice": "crow",
   "level": 25,
   "pan": 0.2
  },
  "... (4 in all)"
 ],
 "tips": [
  "Keep the speaker close to the doorstep and low enough that toddlers' voices stay easy to hear over it.",
  "Keep the whole loop soft and steady, with nothing sudden, so the baby next door is not disturbed."
 ]
}

score - a graveyard yard for primary-school kids, full fright asked: spooky

Input:

{
 "task": "score",
 "scene": "Front yard dressed as an old graveyard: foam headstones, a fog machine behind the hedge, a bare tree hung with paper bats. Mostly primary-school kids walking up the path with their parents.",
 "youngest": "5-9",
 "scare": "3",
 "place": "yard",
 "loop_min": "4",
 "avoid": "[\"no thunder, our dog hides under the bed\"]",
 "blocked": "[\"thunder\"]"
}

Reply:

{
 "task": "score",
 "verdict": "spooky",
 "title": "A Foggy Graveyard Hush",
 "summary": "A spooky mix led by a low drone and wind through the bare tree, with an owl, a crow, a creak, a howl and a distant bell timed across the yard. The foam headstones, the fog machine and the paper bats already set an eerie scene, so the loop leans into a steady, old-graveyard hum rather than anything sudden. With primary-school kids walking up the path with their parents, the mix stays within a spooky level that still lets the scene do the rest of the work.",
 "beds": [
  {
   "voice": "crickets",
   "level": 30,
   "pan": -0.2,
   "why": "a steady night backdrop for the yard"
  },
  {
   "voice": "wind",
   "level": 45,
   "pan": 0.3,
   "why": "a cold breath through the bare tree"
  },
  {
   "voice": "drone",
   "level": 55,
   "pan": 0.0,
   "why": "a low rumble for the old graveyard feel"
  }
 ],
 "events": [
  {
   "at": 20,
   "voice": "creak",
   "level": 40,
   "pan": -0.4
  },
  {
   "at": 70,
   "voice": "owl",
   "level": 35,
   "pan": 0.6
  },
  {
   "at": 130,
   "voice": "crow",
   "level": 30,
   "pan": -0.6
  },
  "... (5 in all)"
 ],
 "tips": [
  "Set the speaker near the yard display, loud enough to carry the creak and the bell up the path without drowning out the kids' voices.",
  "Leave thunder out of the loop as asked, and let the drone and the howl carry the fright instead."
 ]
}

score - a high-school garage walk-through: fright

Input:

{
 "task": "score",
 "scene": "Garage turned into a haunted walk-through for the high-school crowd: black plastic walls, one red bulb, a coffin prop by the exit and a cobwebbed workbench.",
 "youngest": "teens-adults",
 "scare": "3",
 "place": "indoors",
 "loop_min": "4",
 "avoid": "[]",
 "blocked": "[]"
}

Reply:

{
 "task": "score",
 "verdict": "fright",
 "title": "Chains and Whispers in the Garage",
 "summary": "A full-fright mix built on a low drone and a slow heartbeat, with a creak, three knocks, rattling chains and a breathy whisper spread through the loop. The black plastic walls, the red bulb, the coffin prop and the cobwebbed workbench already set a heavy scene, so the loop leans into the darkest layers without easing off. Built for the high-school crowd, nothing here needs softening.",
 "beds": [
  {
   "voice": "drone",
   "level": 60,
   "pan": 0.0,
   "why": "a low hum to match the black plastic walls"
  },
  {
   "voice": "heartbeat",
   "level": 50,
   "pan": -0.2,
   "why": "a slow pulse for the walk-through nerves"
  }
 ],
 "events": [
  {
   "at": 30,
   "voice": "creak",
   "level": 55,
   "pan": 0.3
  },
  {
   "at": 90,
   "voice": "knock",
   "level": 70,
   "pan": -0.4
  },
  {
   "at": 150,
   "voice": "chains",
   "level": 80,
   "pan": 0.5
  },
  "... (4 in all)"
 ],
 "tips": [
  "Run the loop loud enough to fill the black plastic walls without drowning out the group's own reactions.",
  "Since the walk-through is indoors, keep the heavier layers like the drone and the chains strong enough to carry over the crowd's own noise."
 ]
}

check - a graveyard mix that fits: keep

Input:

{
 "task": "check",
 "scene": "Front yard dressed as an old graveyard: foam headstones, a fog machine behind the hedge, a bare tree hung with paper bats. Mostly primary-school kids walking up the path with their parents.",
 "youngest": "5-9",
 "scare": "3",
 "place": "yard",
 "loop_min": "4",
 "avoid": "[\"no thunder, our dog hides under the bed\"]",
 "blocked": "[\"thunder\"]",
 "mix": "{\"beds\":[{\"voice\":\"wind\",\"level\":45,\"pan\":-0.3},{\"voice\":\"crickets\",\"level\":30,\"pan\":0.4}],\"events\":[{\"at\":15,\"voice\":\"owl\",\"level\":35,\"pan\":0.6},{\"at\":50,\"voice\":\"creak\",\"level\":50,\"pan\":-0.5},{\"at\":92,\"voice\":\"crow\",\"level\":40,\"pan\":0.7},{\"at\":130,\"voice\":\"bell\",\"level\":45,\"pan\":0},{\"at\":176,\"voice\":\"howl\",\"level\":55,\"pan\":-0.6},{\"at\":214,\"voice\":\"owl\",\"le ... (380 characters)",
 "page_findings": "[]"
}

Reply:

{
 "task": "check",
 "verdict": "keep",
 "title": "Creaking Wind Over The Headstones",
 "summary": "This mix keeps every voice at or under the level the brief allows, with the wind and crickets beds sitting under well-spaced owl, creak, crow, bell and howl cues for the graveyard yard. The leave-out list is respected and the loop gives the primary-school crowd room to breathe, so it stays exactly as built.",
 "issues": [],
 "mix": {
  "beds": [],
  "events": []
 },
 "hears": "Walking up the path, the youngest guest hears steady wind and crickets under an owl hooting, a slow door creak, a crow cawing, a distant bell tolling twice and a far-off howl, with the owl returning near the end of the loop."
}

check - a howl over the cap and a crow 3 seconds after it: adjust

Input:

{
 "task": "check",
 "scene": "Front yard dressed as an old graveyard: foam headstones, a fog machine behind the hedge, a bare tree hung with paper bats. Mostly primary-school kids walking up the path with their parents.",
 "youngest": "5-9",
 "scare": "3",
 "place": "yard",
 "loop_min": "4",
 "avoid": "[\"no thunder, our dog hides under the bed\"]",
 "blocked": "[\"thunder\"]",
 "mix": "{\"beds\":[{\"voice\":\"wind\",\"level\":50,\"pan\":-0.3},{\"voice\":\"drone\",\"level\":40,\"pan\":0}],\"events\":[{\"at\":20,\"voice\":\"owl\",\"level\":35,\"pan\":0.5},{\"at\":60,\"voice\":\"creak\",\"level\":50,\"pan\":-0.4},{\"at\":100,\"voice\":\"howl\",\"level\":88,\"pan\":-0.6},{\"at\":103,\"voice\":\"crow\",\"level\":40,\"pan\":0.6},{\"at\":160,\"voice\":\"bell\",\"level\":45,\"pan\":0},{\"at\":210,\"voice\":\"owl\",\"level\" ... (376 characters)",
 "page_findings": "[{\"rule\":\"too-loud\",\"voice\":\"howl\",\"at\":100},{\"rule\":\"crowded\",\"voice\":\"crow\",\"at\":103}]"
}

Reply:

{
 "task": "check",
 "verdict": "adjust",
 "title": "Wind, Drone And Howl Over The Headstones",
 "summary": "This yard mix needs two small fixes: the far-off howl is louder than the cap for this level, and the crow cawing right after it comes in too close behind. With the howl turned down to the cap and the crow moved out to keep its gap, the wind and drone beds and the rest of the cues stay exactly as built.",
 "issues": [
  {
   "rule": "too-loud",
   "voice": "howl",
   "note": "The far-off howl is above the cap of 75 for this level, so its level comes down to 75."
  },
  {
   "rule": "crowded",
   "voice": "crow",
   "note": "The crow cawing lands less than 6 seconds after the howl before it, so it moves out to keep the 6-second gap."
  }
 ],
 "mix": {
  "beds": [
   {
    "voice": "wind",
    "level": 50,
    "pan": -0.3
   },
   {
    "voice": "drone",
    "level": 40,
    "pan": 0
   }
  ],
  "events": [
   {
    "at": 20,
    "voice": "owl",
    "level": 35,
    "pan": 0.5
   },
   {
    "at": 60,
    "voice": "creak",
    "level": 50,
    "pan": -0.4
   },
   {
    "at": 100,
    "voice": "howl",
    "level": 75,
    "pan": -0.6
   },
   "... (6 in all)"
  ]
 },
 "hears": "Walking up, the youngest guest hears wind and a low cellar drone under an owl hooting, a slow door creak, a far-off howl, a crow cawing, a distant bell tolling twice and another owl hooting near the end."
}

check - thunder, a heartbeat and a whisper for toddlers: rework

Input:

{
 "task": "check",
 "scene": "Front step of our townhouse: one lit pumpkin, a straw bale and a bowl of candy on a stool. The playgroup toddlers come by first, around dusk, with their parents.",
 "youngest": "under-5",
 "scare": "2",
 "place": "doorstep",
 "loop_min": "2",
 "avoid": "[\"nothing sudden - the baby next door naps late\"]",
 "blocked": "[\"knock\",\"thunder\",\"chains\"]",
 "mix": "{\"beds\":[{\"voice\":\"heartbeat\",\"level\":60,\"pan\":0},{\"voice\":\"rain\",\"level\":40,\"pan\":0}],\"events\":[{\"at\":20,\"voice\":\"thunder\",\"level\":80,\"pan\":0},{\"at\":50,\"voice\":\"owl\",\"level\":40,\"pan\":0.5},{\"at\":80,\"voice\":\"whisper\",\"level\":50,\"pan\":-0.5},{\"at\":100,\"voice\":\"chime\",\"level\":40,\"pan\":0.3}]}",
 "page_findings": "[{\"rule\":\"avoided\",\"voice\":\"thunder\"},{\"rule\":\"too-scary\",\"voice\":\"heartbeat\"},{\"rule\":\"too-scary\",\"voice\":\"thunder\"},{\"rule\":\"too-scary\",\"voice\":\"whisper\"},{\"rule\":\"too-loud\",\"voice\":\"heartbeat\"},{\"rule\":\"too-loud\",\"voice\":\"thunder\",\"at\":20}]"
}

Reply:

{
 "task": "check",
 "verdict": "rework",
 "title": "Rain, Crickets, Owl And Wind Chimes",
 "summary": "This doorstep mix needs a rework: the heartbeat bed, the thunder roll and the breathy whisper are all scarier and louder than suits the youngest guest coming by, and the thunder roll is also on the leave-out list since the baby next door naps late. The new mix drops all three, keeps the rain bed and the owl hooting and wind chimes cues, and adds a crickets bed so the soft layers still fill the loop.",
 "issues": [
  {
   "rule": "avoided",
   "voice": "thunder",
   "note": "The thunder roll is on the leave-out list, since the brief asks for nothing sudden while the baby next door naps late, so it is left out."
  },
  {
   "rule": "too-scary",
   "voice": "heartbeat",
   "note": "The heartbeat bed is rated above what suits the youngest guest at this doorstep, so it is left out."
  },
  {
   "rule": "too-scary",
   "voice": "thunder",
   "note": "The thunder roll is also far scarier than suits the youngest guest, on top of being on the leave-out list."
  },
  "... (6 in all)"
 ],
 "mix": {
  "beds": [
   {
    "voice": "rain",
    "level": 40,
    "pan": 0
   },
   {
    "voice": "crickets",
    "level": 35,
    "pan": -0.3
   }
  ],
  "events": [
   {
    "at": 50,
    "voice": "owl",
    "level": 40,
    "pan": 0.5
   },
   {
    "at": 100,
    "voice": "chime",
    "level": 40,
    "pan": 0.3
   }
  ]
 },
 "hears": "Walking up to the doorstep, the youngest guest hears soft rain and crickets under an owl hooting and a run of wind chimes, with nothing sudden or scary in the loop."
}

Build the input yourself

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

const K = PorchKit;
const form = K.form({ scene: "Front porch with two lit pumpkins; the speaker sits under the bench.", youngest: "5-9", scare: "2",
  place: "doorstep", loop: "4", avoid: "no thunder - the dog hides",
  mix: { beds: [{ voice: "wind", level: 45, pan: -0.3 }], events: [{ at: 20, voice: "owl", level: 40, pan: 0.4 }] } });
console.log(K.levelOf(form), K.blocked(form).ids, K.findings(form)); // 2, ["thunder"], [the sparse loop: one cue in four minutes]
const wire = K.toWire(K.buildInput("check", form).input);         // POST this to /estimate and /run ("score" leaves out the mix)
// then: const res = K.normalize(K.parseReply(job.output.output), "check", form.loop);
// const { notes } = K.check(res, form);                           // the page's re-check of the reply

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://creaky-porch.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":"creaky-porch"}' | 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": "score", "scene": "Front yard dressed as an old graveyard: foam headstones, a fog machine...", "youngest": "5-9", "scare": "3", "place": "yard", "loop_min": "4", "avoid": "[\"no thunder, our dog hides under the bed\"]", "blocked": "[\"thunder\"]"}
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 creaky-porch:<task>:<inputHash>:a<attempt> (the lanes are score and check), 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="creaky-porch:score:$(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: creaky-porch:score:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json