← Nightfold/API
Tokens

Drive the wind-down guide from your own code

The pacing happens in the browser: the page turns a breathing pattern and a segment length into breath slots - one per full breath - and a word budget for each phase at a slow reading pace. The guide over HTTP takes those facts and writes a script to fit them, or reads a session plan and the last few before/after ratings and proposes an adjusted plan. It is wellbeing practice, not medical advice: the guide never diagnoses and never advises on health.

Basics

Error codes

HTTPcodemeaning
400VALIDATION_ERRORThe body is not a JSON object, or a field has the wrong type.
401UNAUTHORIZEDMissing, expired or revoked token. Get a new one on /tokens.html.
402INSUFFICIENT_CREDITSThe balance is below min_credits. Top up at skillsafe.ai/account/billing/.
403FORBIDDENA guest token tried to run. Runs need a personal (signed-in) token.
409IDEMPOTENCY_CONFLICTThe same Idempotency-Key was reused with a different body.
429RATE_LIMITEDSlow down and retry with backoff.

The task field: two lanes

taskwhat it doesreply
scriptWrites a bespoke script for one segment (body scan, grounding, sleep story or visualisation): exactly slot_count short phrases, each tagged with the breath phase it lands on and within that phase's word budget.{"task":"script","title","blurb","phrases":[{"slot","phase","text"}],"closing_note"}
planReviews the session plan and recent before/after log (intent: "review") or makes the same session a little longer (intent: "longer") and returns the full proposed plan with one reason per change.{"task":"plan","headline","plan":[{"id","kind","minutes","pattern","script","reason"}],"removed":[{"id","reason"}],"order_reason","note"}

Input fields for script

fieldtypemeaning
taskstring"script"
segment_kindstringscan, grounding, story or visual
audiencestringadult, child, exam or midday
pattern_idstringone of box, 478, coherent, calm, settle, small
patternJSON string{id, name, cycle_seconds, phases:[{phase, seconds, max_words}]} - phases are in, hold, out, rest
slot_countnumberbreath slots in the segment: one phrase per slot
minutes, words_per_secondnumberthe segment length and the slow pace the budgets assume (1.5)
theme, avoid, tonestringoptional: what it is about, what to leave out, the voice wanted
current_titlestringoptional: the bundled script the segment plays now
clip_note, retry_notestringoptional: what the page cut from a long field; a correction after an unreadable reply

Input fields for plan

fieldtypemeaning
taskstring"plan"
intentstringreview or longer
audience, tone, plan_namestringwho the session is for, the voice wanted, its name
planJSON stringsegments in play order: {id, kind, minutes, pattern?, script?, script_title?}
minutes_now, chime_betweennumber, booleanthe total including 6-second chimes between segments; whether those chimes play
patterns, libraryJSON stringevery pattern and every script the app can play - the only ids a reply may use
logs, log_countJSON string, numberup to 8 recent sessions as {days_ago, plan, planned_min, played_min, completed, before, after, patterns, kinds} - never timestamps
log_note, clip_note, retry_notestringoptional

Worked examples

These are the page's four free examples - the exact bodies the page sends and the replies it replays.

script: a night-train sleep story on 4-7-8, 30 breath slots

Input:

{
 "task": "script",
 "segment_kind": "story",
 "audience": "adult",
 "tone": "",
 "theme": "a slow night train through snowy country, seen from a warm sleeper cabin",
 "avoid": "no stations announced, no other passengers talking",
 "pattern_id": "478",
 "pattern": "(a JSON string - decoded below)",
 "slot_count": 30,
 "minutes": 9.5,
 "words_per_second": 1.5,
 "current_title": "The night harbour"
}

pattern, decoded:

{
 "id": "478",
 "name": "4-7-8",
 "cycle_seconds": 19,
 "phases": [
  {
   "phase": "in",
   "seconds": 4,
   "max_words": 6
  },
  {
   "phase": "hold",
   "seconds": 7,
   "max_words": 10
  },
  {
   "phase": "out",
   "seconds": 8,
   "max_words": 12
  }
 ]
}

Reply:

{
 "task": "script",
 "title": "The Snowbound Sleeper",
 "blurb": "A slow night train through snowy country, watched from a warm bunk as the world goes quiet.",
 "phrases": [
  {
   "slot": 1,
   "phase": "out",
   "text": "You are lying in a narrow bunk, under a heavy wool blanket."
  },
  {
   "slot": 2,
   "phase": "in",
   "text": "The cabin is small and warm."
  },
  {
   "slot": 3,
   "phase": "out",
   "text": "A little lamp glows amber beside the window, turned down low."
  },
  {
   "slot": 4,
   "phase": "hold",
   "text": "Outside, the snow keeps falling, soft and slow."
  },
  {
   "slot": 5,
   "phase": "out",
   "text": "The train moves through the night at an easy, unhurried pace."
  },
  {
   "slot": 6,
   "phase": "in",
   "text": "Notice the gentle sway beneath you."
  },
  {
   "slot": 7,
   "phase": "out",
   "text": "Side to side, side to side, like a cradle rocking slowly."
  },
  {
   "slot": 8,
   "phase": "out",
   "text": "The wheels hum on the rails, one low note, over and over."
  },
  {
   "slot": 9,
   "phase": "hold",
   "text": "Beyond the glass, white fields stretch away, quiet and wide."
  },
  {
   "slot": 10,
   "phase": "out",
   "text": "A line of dark pine trees drifts past, their branches dusted white."
  },
  {
   "slot": 11,
   "phase": "in",
   "text": "Feel the warmth of the blanket."
  },
  {
   "slot": 12,
   "phase": "out",
   "text": "Somewhere below, the heating ticks softly, keeping the little room cosy."
  },
  {
   "slot": 13,
   "phase": "hold",
   "text": "A farmhouse light glows far off, then slips behind."
  },
  {
   "slot": 14,
   "phase": "out",
   "text": "Snow on the window ledge grows a little deeper, a little rounder."
  },
  {
   "slot": 15,
   "phase": "out",
   "text": "There is nowhere you need to be. The train knows the way."
  },
  {
   "slot": 16,
   "phase": "in",
   "text": "The rocking carries on, steady."
  },
  {
   "slot": 17,
   "phase": "out",
   "text": "A frozen river passes, still and silver beneath the moon."
  },
  {
   "slot": 18,
   "phase": "hold",
   "text": "The snow falls on the river, and on the trees."
  },
  {
   "slot": 19,
   "phase": "out",
   "text": "The hum of the wheels grows softer, further away."
  },
  {
   "slot": 20,
   "phase": "out",
   "text": "Your hands are heavy and warm on the blanket."
  },
  {
   "slot": 21,
   "phase": "in",
   "text": "The lamp glows a little dimmer."
  },
  {
   "slot": 22,
   "phase": "out",
   "text": "Outside, the hills are round and white and sleeping."
  },
  {
   "slot": 23,
   "phase": "hold",
   "text": "Only snow now, falling and falling."
  },
  {
   "slot": 24,
   "phase": "out",
   "text": "The fields, the snow, the slow rocking."
  },
  {
   "slot": 25,
   "phase": "out",
   "text": "Softer still."
  },
  {
   "slot": 26,
   "phase": "in",
   "text": "One more slow breath."
  },
  {
   "slot": 27,
   "phase": "out",
   "text": "The window fills with quiet white."
  },
  {
   "slot": 28,
   "phase": "hold",
   "text": "Nothing to watch."
  },
  {
   "slot": 29,
   "phase": "out",
   "text": "Let the train carry you."
  },
  {
   "slot": 30,
   "phase": "out",
   "text": "Rest now."
  }
 ],
 "closing_note": ""
}

script: a child's body scan about a sleepy cat, small breaths, 20 slots

Input:

{
 "task": "script",
 "segment_kind": "scan",
 "audience": "child",
 "tone": "simple words for a four-year-old",
 "theme": "a sleepy cat stretching and curling up, part by part",
 "avoid": "",
 "pattern_id": "small",
 "pattern": "(a JSON string - decoded below)",
 "slot_count": 20,
 "minutes": 2.3,
 "words_per_second": 1.5,
 "current_title": "Floppy teddy"
}

pattern, decoded:

{
 "id": "small",
 "name": "Small breaths 3-4",
 "cycle_seconds": 7,
 "phases": [
  {
   "phase": "in",
   "seconds": 3,
   "max_words": 4
  },
  {
   "phase": "out",
   "seconds": 4,
   "max_words": 6
  }
 ]
}

Reply:

{
 "task": "script",
 "title": "Curl Up Like a Cat",
 "blurb": "A tiny bedtime body scan: a sleepy cat stretches, softens and curls up, part by part.",
 "phrases": [
  {
   "slot": 1,
   "phase": "out",
   "text": "Let's be a sleepy little cat."
  },
  {
   "slot": 2,
   "phase": "in",
   "text": "Stretch your back paws."
  },
  {
   "slot": 3,
   "phase": "out",
   "text": "Now let them go soft."
  },
  {
   "slot": 4,
   "phase": "in",
   "text": "Stretch your long legs."
  },
  {
   "slot": 5,
   "phase": "out",
   "text": "Floppy legs, sleepy legs."
  },
  {
   "slot": 6,
   "phase": "in",
   "text": "Breathe into your tummy."
  },
  {
   "slot": 7,
   "phase": "out",
   "text": "Your tummy is warm and round."
  },
  {
   "slot": 8,
   "phase": "out",
   "text": "Your back sinks into the bed."
  },
  {
   "slot": 9,
   "phase": "in",
   "text": "Stretch your front paws."
  },
  {
   "slot": 10,
   "phase": "out",
   "text": "Now your paws go soft."
  },
  {
   "slot": 11,
   "phase": "in",
   "text": "Shrug up your shoulders."
  },
  {
   "slot": 12,
   "phase": "out",
   "text": "Let them drop down, down, down."
  },
  {
   "slot": 13,
   "phase": "out",
   "text": "Your sleepy head is heavy."
  },
  {
   "slot": 14,
   "phase": "in",
   "text": "Wiggle your little whiskers."
  },
  {
   "slot": 15,
   "phase": "out",
   "text": "Now your face goes still."
  },
  {
   "slot": 16,
   "phase": "out",
   "text": "Your ears are soft and quiet."
  },
  {
   "slot": 17,
   "phase": "out",
   "text": "Curl your tail around you."
  },
  {
   "slot": 18,
   "phase": "in",
   "text": "Cosy, cosy cat."
  },
  {
   "slot": 19,
   "phase": "out",
   "text": "Curled up warm, nose to tail."
  },
  {
   "slot": 20,
   "phase": "out",
   "text": "Goodnight paws, goodnight tail, goodnight cat."
  }
 ],
 "closing_note": ""
}

plan (intent review): a fortnight of evenings where the story is often abandoned

Input:

{
 "task": "plan",
 "intent": "review",
 "audience": "adult",
 "tone": "",
 "plan_name": "Evening wind-down",
 "plan": "(a JSON string - decoded below)",
 "minutes_now": 20.4,
 "chime_between": true,
 "patterns": "(a JSON string - decoded below)",
 "library": "(a JSON string - decoded below)",
 "logs": "(a JSON string - decoded below)",
 "log_count": 9,
 "log_note": "I usually drift off or give up somewhere in the story.",
 "clip_note": "sent the latest 8 of 9 sessions; the 1 older ones are not listed"
}

plan, decoded:

[
 {
  "id": "s1",
  "kind": "breath",
  "minutes": 2,
  "pattern": "calm"
 },
 {
  "id": "s2",
  "kind": "scan",
  "minutes": 5.7,
  "pattern": "calm",
  "script": "scan-evening",
  "script_title": "Evening body scan"
 },
 {
  "id": "s3",
  "kind": "silence",
  "minutes": 1
 },
 {
  "id": "s4",
  "kind": "story",
  "minutes": 11.4,
  "pattern": "478",
  "script": "story-harbour",
  "script_title": "The night harbour"
 }
]

patterns, decoded:

[
 {
  "id": "box",
  "name": "Box 4-4-4-4",
  "cycle_seconds": 16,
  "phases": [
   {
    "phase": "in",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "hold",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "out",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "rest",
    "seconds": 4,
    "max_words": 6
   }
  ]
 },
 {
  "id": "478",
  "name": "4-7-8",
  "cycle_seconds": 19,
  "phases": [
   {
    "phase": "in",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "hold",
    "seconds": 7,
    "max_words": 10
   },
   {
    "phase": "out",
    "seconds": 8,
    "max_words": 12
   }
  ]
 },
 {
  "id": "coherent",
  "name": "Coherent 5.5",
  "cycle_seconds": 11,
  "phases": [
   {
    "phase": "in",
    "seconds": 5.5,
    "max_words": 8
   },
   {
    "phase": "out",
    "seconds": 5.5,
    "max_words": 8
   }
  ]
 },
 {
  "id": "calm",
  "name": "Calm 4-6",
  "cycle_seconds": 10,
  "phases": [
   {
    "phase": "in",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "out",
    "seconds": 6,
    "max_words": 9
   }
  ]
 },
 {
  "id": "settle",
  "name": "Settle 4-2-6",
  "cycle_seconds": 12,
  "phases": [
   {
    "phase": "in",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "hold",
    "seconds": 2,
    "max_words": 3
   },
   {
    "phase": "out",
    "seconds": 6,
    "max_words": 9
   }
  ]
 },
 {
  "id": "small",
  "name": "Small breaths 3-4",
  "cycle_seconds": 7,
  "phases": [
   {
    "phase": "in",
    "seconds": 3,
    "max_words": 4
   },
   {
    "phase": "out",
    "seconds": 4,
    "max_words": 6
   }
  ]
 }
]

library, decoded:

[
 {
  "id": "scan-evening",
  "kind": "scan",
  "title": "Evening body scan",
  "audience": "adult",
  "pattern": "calm",
  "phrases": 34
 },
 {
  "id": "ground-senses",
  "kind": "grounding",
  "title": "Five senses, slowly",
  "audience": "adult",
  "pattern": "box",
  "phrases": 26
 },
 {
  "id": "exam-steady",
  "kind": "grounding",
  "title": "Before the exam",
  "audience": "exam",
  "pattern": "settle",
  "phrases": 21
 },
 {
  "id": "story-harbour",
  "kind": "story",
  "title": "The night harbour",
  "audience": "adult",
  "pattern": "478",
  "phrases": 36
 },
 {
  "id": "story-fox",
  "kind": "story",
  "title": "The sleepy fox",
  "audience": "child",
  "pattern": "small",
  "phrases": 33
 },
 {
  "id": "visual-lake",
  "kind": "visual",
  "title": "The warm lake",
  "audience": "adult",
  "pattern": "coherent",
  "phrases": 27
 },
 {
  "id": "scan-floppy",
  "kind": "scan",
  "title": "Floppy teddy",
  "audience": "child",
  "pattern": "small",
  "phrases": 24
 },
 {
  "id": "midday-reset",
  "kind": "grounding",
  "title": "Midday reset",
  "audience": "adult",
  "pattern": "coherent",
  "phrases": 17
 }
]

logs, decoded:

[
 {
  "days_ago": 0,
  "plan": "Evening wind-down",
  "planned_min": 20.4,
  "played_min": 10.2,
  "completed": false,
  "before": 2,
  "after": 3,
  "patterns": [
   "calm",
   "478"
  ],
  "kinds": [
   "breath",
   "scan",
   "silence",
   "story"
  ]
 },
 {
  "days_ago": 1,
  "plan": "Evening wind-down",
  "planned_min": 20.4,
  "played_min": 20.4,
  "completed": true,
  "before": 2,
  "after": 4,
  "patterns": [
   "calm",
   "478"
  ],
  "kinds": [
   "breath",
   "scan",
   "silence",
   "story"
  ]
 },
 {
  "days_ago": 2,
  "plan": "Evening wind-down",
  "planned_min": 20.4,
  "played_min": 11.7,
  "completed": false,
  "before": 3,
  "after": 3,
  "patterns": [
   "calm",
   "478"
  ],
  "kinds": [
   "breath",
   "scan",
   "silence",
   "story"
  ]
 },
 {
  "days_ago": 4,
  "plan": "Evening wind-down",
  "planned_min": 20.4,
  "played_min": 10.7,
  "completed": false,
  "before": 2,
  "after": 3,
  "patterns": [
   "calm",
   "478"
  ],
  "kinds": [
   "breath",
   "scan",
   "silence",
   "story"
  ]
 },
 {
  "days_ago": 5,
  "plan": "Evening wind-down",
  "planned_min": 20.4,
  "played_min": 20.4,
  "completed": true,
  "before": 1,
  "after": 4,
  "patterns": [
   "calm",
   "478"
  ],
  "kinds": [
   "breath",
   "scan",
   "silence",
   "story"
  ]
 },
 {
  "days_ago": 7,
  "plan": "Evening wind-down",
  "planned_min": 20.4,
  "played_min": 9.8,
  "completed": false,
  "before": 3,
  "after": 3,
  "patterns": [
   "calm",
   "478"
  ],
  "kinds": [
   "breath",
   "scan",
   "silence",
   "story"
  ]
 },
 {
  "days_ago": 9,
  "plan": "Evening wind-down",
  "planned_min": 20.4,
  "played_min": 20.4,
  "completed": true,
  "before": 2,
  "after": 4,
  "patterns": [
   "calm",
   "478"
  ],
  "kinds": [
   "breath",
   "scan",
   "silence",
   "story"
  ]
 },
 {
  "days_ago": 10,
  "plan": "Evening wind-down",
  "planned_min": 20.4,
  "played_min": 11,
  "completed": false,
  "before": 2,
  "after": null,
  "patterns": [
   "calm",
   "478"
  ],
  "kinds": [
   "breath",
   "scan",
   "silence",
   "story"
  ]
 }
]

Reply:

{
 "task": "plan",
 "headline": "Keep the breathing and body scan that work, and play the story on Calm 4-6 instead of 4-7-8, so it is shorter and has no long holds.",
 "plan": [
  {
   "id": "s1",
   "kind": "breath",
   "minutes": 2,
   "pattern": "calm",
   "script": "",
   "reason": ""
  },
  {
   "id": "s2",
   "kind": "scan",
   "minutes": 5.7,
   "pattern": "calm",
   "script": "scan-evening",
   "reason": ""
  },
  {
   "id": "s3",
   "kind": "silence",
   "minutes": 1,
   "pattern": "",
   "script": "",
   "reason": ""
  },
  {
   "id": "s4",
   "kind": "story",
   "minutes": 6,
   "pattern": "calm",
   "script": "story-harbour",
   "reason": "5 of 8 sessions stopped early in the story; Calm 4-6 has no 7-second hold and fits 36 phrases in 6 min."
  }
 ],
 "removed": [],
 "order_reason": "",
 "note": "Drifting off during the story is fine; there is no need to reach the end."
}

plan (intent longer): the exam session tomorrow, a little longer

Input:

{
 "task": "plan",
 "intent": "longer",
 "audience": "exam",
 "tone": "",
 "plan_name": "Before the exam",
 "plan": "(a JSON string - decoded below)",
 "minutes_now": 7,
 "chime_between": true,
 "patterns": "(a JSON string - decoded below)",
 "library": "(a JSON string - decoded below)",
 "logs": "[]",
 "log_count": 0,
 "log_note": ""
}

plan, decoded:

[
 {
  "id": "e1",
  "kind": "breath",
  "minutes": 2.1,
  "pattern": "box"
 },
 {
  "id": "e2",
  "kind": "grounding",
  "minutes": 4.2,
  "pattern": "settle",
  "script": "exam-steady",
  "script_title": "Before the exam"
 },
 {
  "id": "e3",
  "kind": "silence",
  "minutes": 0.5
 }
]

patterns, decoded:

[
 {
  "id": "box",
  "name": "Box 4-4-4-4",
  "cycle_seconds": 16,
  "phases": [
   {
    "phase": "in",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "hold",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "out",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "rest",
    "seconds": 4,
    "max_words": 6
   }
  ]
 },
 {
  "id": "478",
  "name": "4-7-8",
  "cycle_seconds": 19,
  "phases": [
   {
    "phase": "in",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "hold",
    "seconds": 7,
    "max_words": 10
   },
   {
    "phase": "out",
    "seconds": 8,
    "max_words": 12
   }
  ]
 },
 {
  "id": "coherent",
  "name": "Coherent 5.5",
  "cycle_seconds": 11,
  "phases": [
   {
    "phase": "in",
    "seconds": 5.5,
    "max_words": 8
   },
   {
    "phase": "out",
    "seconds": 5.5,
    "max_words": 8
   }
  ]
 },
 {
  "id": "calm",
  "name": "Calm 4-6",
  "cycle_seconds": 10,
  "phases": [
   {
    "phase": "in",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "out",
    "seconds": 6,
    "max_words": 9
   }
  ]
 },
 {
  "id": "settle",
  "name": "Settle 4-2-6",
  "cycle_seconds": 12,
  "phases": [
   {
    "phase": "in",
    "seconds": 4,
    "max_words": 6
   },
   {
    "phase": "hold",
    "seconds": 2,
    "max_words": 3
   },
   {
    "phase": "out",
    "seconds": 6,
    "max_words": 9
   }
  ]
 },
 {
  "id": "small",
  "name": "Small breaths 3-4",
  "cycle_seconds": 7,
  "phases": [
   {
    "phase": "in",
    "seconds": 3,
    "max_words": 4
   },
   {
    "phase": "out",
    "seconds": 4,
    "max_words": 6
   }
  ]
 }
]

library, decoded:

[
 {
  "id": "scan-evening",
  "kind": "scan",
  "title": "Evening body scan",
  "audience": "adult",
  "pattern": "calm",
  "phrases": 34
 },
 {
  "id": "ground-senses",
  "kind": "grounding",
  "title": "Five senses, slowly",
  "audience": "adult",
  "pattern": "box",
  "phrases": 26
 },
 {
  "id": "exam-steady",
  "kind": "grounding",
  "title": "Before the exam",
  "audience": "exam",
  "pattern": "settle",
  "phrases": 21
 },
 {
  "id": "story-harbour",
  "kind": "story",
  "title": "The night harbour",
  "audience": "adult",
  "pattern": "478",
  "phrases": 36
 },
 {
  "id": "story-fox",
  "kind": "story",
  "title": "The sleepy fox",
  "audience": "child",
  "pattern": "small",
  "phrases": 33
 },
 {
  "id": "visual-lake",
  "kind": "visual",
  "title": "The warm lake",
  "audience": "adult",
  "pattern": "coherent",
  "phrases": 27
 },
 {
  "id": "scan-floppy",
  "kind": "scan",
  "title": "Floppy teddy",
  "audience": "child",
  "pattern": "small",
  "phrases": 24
 },
 {
  "id": "midday-reset",
  "kind": "grounding",
  "title": "Midday reset",
  "audience": "adult",
  "pattern": "coherent",
  "phrases": 17
 }
]

Reply:

{
 "task": "plan",
 "headline": "The same session about a minute longer: more box breathing to start and a longer silence at the end, with the grounding script unchanged.",
 "plan": [
  {
   "id": "e1",
   "kind": "breath",
   "minutes": 2.7,
   "pattern": "box",
   "script": "",
   "reason": "Lengthened from 2.1 to 2.7 min, about 10 box cycles, to give a steadier start."
  },
  {
   "id": "e2",
   "kind": "grounding",
   "minutes": 4.2,
   "pattern": "settle",
   "script": "exam-steady",
   "reason": ""
  },
  {
   "id": "e3",
   "kind": "silence",
   "minutes": 1,
   "pattern": "",
   "script": "",
   "reason": "Doubled from 0.5 to 1 min, so the steadiness from the script has room to settle before you begin."
  }
 ],
 "removed": [],
 "order_reason": "",
 "note": ""
}

The page reconciles every reply; over the API, so should you

The page's own code is plain scripts you can load in node after global.window = {}, in this order: breath.js (patterns, phase timing, Breath.place - the phrase-slot engine that budgets words per phase and splits overlong phrases), scripts.js (the bundled library), plan.js (Plan.reconcilePlan turns a proposed plan into separate changes and flags unknown patterns, unknown scripts, out-of-range minutes and missing reasons), guide.js (Guide.buildInput builds these exact run bodies; Guide.normalize checks a reply).

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://nightfold.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":"nightfold"}' | 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 (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.

# input.json: the run input object - {"task": "script", "segment_kind": "story", "pattern_id": "478", "slot_count": 30, "pattern": "...", ...}
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 task and the input: a retry with the same key never bills twice. The page uses nightfold:<task>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="nightfold:script:$(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: nightfold:script:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json