← Sleep Low
Sleep Low API
Tokens

Drive Sleep Low from your own code

The one metered step - the pace check of one high-altitude trek itinerary - is available over HTTP. A natural use is a trip-planning sheet or an operator's itinerary builder: your code sends the nights with the page's pace check, and gets back the nights to add (a second night somewhere, or a new stop between two nights), warning signs for the highest nights, turnaround rules, questions for the guide and an on pace / add nights / rethink verdict.

The split first: the page walks the nights; the model judges; the page re-walks the model's changes. Reading the itinerary, the gain in sleeping altitude each night, the pacing flags and the page's own minimum repair come from pacekit.js, which runs in node as well as in the browser. Its check function then inserts the reply's nights into the plan itself: an extra night repeats the night before it, a new stop must sit between its two neighbours, a stop the reply names with a place not in your itinerary is shown as an altitude only, and an "on pace" or "add nights" verdict whose revised plan still breaks a rule is flagged.

The pacing rules are common wilderness-medicine guidance, not medical advice: above 3,000 m, sleep no more than 500 m higher than the night before; take a night that does not climb at least every fourth night; and do not fly or drive straight to a first night at 3,500 m or more.

Get a token on the token page (step 1 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":"sleep-low"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task 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 pace check 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 comes first

Every run sends task. There is one task, pace. It returns title, verdict, summary; changes [{after_day, action, place, sleep_m, why}] (in the order they happen); watch [{sign, act}] (3-5); turnaround (2-4); ask (2-4); caveats (0-3).

Verdict (checked in this order)Meaning
rethinkNight 1 sleeps at 3,500 m or more after climbing over 500 m (a fly-in or drive-in start), or the page's repair needs four or more extra nights, or any night sleeps at 5,500 m (about 18,000 ft) or higher - expedition altitude, never on pace. The fix is a different start or high point; changes is empty.
add_nightsOtherwise, a night climbs more than 500 m above 3,000 m or is a fourth climbing night in a row - and one to three added nights fix it.
on_paceNo night breaks a rule. changes is empty unless the notes ask for a spare night.
ActionWhat the page does with it
restA second night right after planned night after_day (0 = after the start), at that night's place and altitude.
stopA new night after night after_day at sleep_m (always metres), which must lie strictly between that night and the next one. place stays empty unless your itinerary or notes name one.

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (PaceKit.toWire does this). Only task is required by the schema.

Input fields

FieldTypeNotes
taskstringAlways pace.
todaystringYYYY-MM-DD.
tripstringThe trek's name (may be empty).
unitstringm or ft - the unit the itinerary is written in.
itinerarystringOne night a line: place | sleeping altitude | highest point that day | note. Past 5,000 characters the middle is cut on whole lines around a marker.
startstring (JSON){place, alt, alt_m, assumed} - where you sleep the night before night 1.
nightsstring (JSON)[{day, place, sleep, sleep_m, high, high_m, gain, gain_m, note, flags}]; flags are arrival, jump, big_jump, no_rest, rest, climb_high.
factsstring (JSON){unit, nights, start_place, start_m, start_assumed, history, max_sleep, max_sleep_m, max_sleep_day, max_sleep_place, max_high_m, max_gain_m, max_gain_day, max_gain_above_3000, max_gain_above_3000_m, max_gain_above_3000_day, longest_climb_run, nights_above_3000, first_above_day, avg_gain_above_3000_m, jumps, no_rest, arrival, rest_nights, climb_high_days, extra_nights, repaired_nights, repair_breaks}.
page_repairstring (JSON)[{after_day, action, place, sleep, sleep_m}] - the fewest nights that keep the plan to the rules.
page_readsstring (JSON)[{level, code, text}]; level is fix or info.
page_verdictstringThe page's own reading: on_pace, add_nights or rethink.
notesstringOptional, your own comments; past 600 characters the middle is cut.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with PaceKit.parseReply, then normalize, then check it with check(reply, state), which returns {rows, applied, revised, walk, breaks, notes}: one row per change (placed, or why it was not), the revised list of nights, the revised walk with its flags, the nights that still break a rule, and the notes (ok, info, warn, bad) the page shows under "The page's check".

Worked examples

Replies produced against the app's prompt for the sample treks (made-up itineraries with made-up places; lists shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons. Inputs are shown with the long fields folded.

task: pace - Valley circuit - on pace

Input:

{
 "task": "pace",
 "today": "2026-10-21",
 "trip": "Valley circuit with a glacier day",
 "unit": "m",
 "itinerary": "Pine Bridge | 2610 m\nChorten Hill | 3020 m | 3200 m\nChorten Hill | 3020 m | 3450 m | acclimatisation hike\nYak Meadow | 3480 m\nJuniper Flats | 3910 m\nJuniper Fla ... (342 characters)",
 "start": "{\"place\":\"Riverside town\",\"alt\":1350,\"alt_m\":1350,\"assumed\":false}",
 "nights": "[{\"day\":1,\"place\":\"Pine Bridge\",\"sleep\":2610,\"sleep_m\":2610,\"high\":null,\"high_m\":null,\"gain\":1260,\"gain_m\":1260,\"note\":\"\",\"flags\":[]},{\"day\":2,\"place\":\"Chorten Hill\",\"sleep\":3020,\"sleep_m\":3020,\"high\":3200,\"high_m\":3200,\"gain\":410,\"gain_m\":410,\"note\":\"\",\"flags\":[\"climb_high\"]},\"... 9 more\"]",
 "facts": "{\"unit\":\"m\",\"nights\":11,\"start_place\":\"Riverside town\",\"start_m\":1350,\"start_assumed\":false,\"history\":\"none\",\"max_sleep_m\":4790,\"max_sleep_day\":8,\"max_sleep_place\":\"Glacier Camp\",\"max_high_m\":5050,\"max_gain_m\":1260,\"max_ ...}",
 "page_repair": "[]",
 "page_reads": "[{\"level\":\"info\",\"code\":\"climb_high\",\"text\":\"Climb high, sleep low on 4 days (day 2, day 3, day 6, day 8) - going higher by day and sleeping lower helps acclimatisation.\"},{\"level\":\"info\",\"code\":\"rest\",\"text\":\"2 extra nights above 3,000 m at the same altitude (night 3, night 6).\"},\"... 1 more\"]",
 "page_verdict": "on_pace",
 "notes": "Two of us, first time above 4,000 m. Teahouse lodges every night."
}

Reply:

{
 "task": "pace",
 "title": "Valley Circuit With a Glacier Day",
 "verdict": "on_pace",
 "summary": "This plan is on pace: above 3,000 m, climbing nights never run more than two in a row, no night gains more than 460 m above 3,000 m, and the highest night sleeps at 4,790 m, at Glacier Camp. The two rest nights, at Chorten Hill and Juniper Flats, each paired with a climb-high hike before heading on, are what keep the pace steady on the way up to Glacier Camp and back down through Stone Huts.",
 "changes": [],
 "watch": [
  {
   "sign": "Headache along with nausea, tiredness, dizziness or poor sleep at Stone Huts (4,370 m) or Glacier Camp (4,790 m), the two highest nights.",
   "act": "Stop climbing until it clears; go down if it gets worse."
  },
  {
   "sign": "Confusion, stumbling or clumsy walking, or unusual drowsiness, especially around the night at Glacier Camp or on the dawn climb to the viewpoint at 5,050 m.",
   "act": "Go down at once, with help, and get medical care."
  },
  "..."
 ],
 "turnaround": [
  "If signs of altitude sickness appear at Juniper Flats (3,910 m) or higher, stay at that altitude until they clear before going higher - do not continue on to Stone Huts or Glacier Camp.",
  "If a night at Stone Huts (4,370 m) feels worse rather than better, delay or skip the push to Glacier Camp (4,790 m).",
  "..."
 ],
 "ask": [
  "Are there spare days built into the price if the pace needs to slow near Stone Huts or Glacier Camp?",
  "What are the descent and evacuation options from Stone Huts and Glacier Camp if someone needs to go down quickly?",
  "..."
 ],
 "caveats": [
  "Altitudes are taken as entered in the itinerary.",
  "..."
 ]
}

task: pace - Ridge trek in feet - add nights

Input:

{
 "task": "pace",
 "today": "2026-10-21",
 "trip": "Ridge trek to the high col",
 "unit": "ft",
 "itinerary": "Cedar Lodge | 8,900 ft\nHanging Bridge | 10,800 ft\nKettle Village | 12,100 ft\nUpper Pastures | 13,500 ft\nMoraine Camp | 15,100 ft\nHigh Col Camp | 16,400 ft | 17, ... (235 characters)",
 "start": "{\"place\":\"Trailhead town\",\"alt\":7200,\"alt_m\":2195,\"assumed\":false}",
 "nights": "[{\"day\":1,\"place\":\"Cedar Lodge\",\"sleep\":8900,\"sleep_m\":2713,\"high\":null,\"high_m\":null,\"gain\":1700,\"gain_m\":518,\"note\":\"\",\"flags\":[]},{\"day\":2,\"place\":\"Hanging Bridge\",\"sleep\":10800,\"sleep_m\":3292,\"high\":null,\"high_m\":null,\"gain\":1900,\"gain_m\":579,\"note\":\"\",\"flags\":[\"jump\"]},\"... 6 more\"]",
 "facts": "{\"unit\":\"ft\",\"nights\":8,\"start_place\":\"Trailhead town\",\"start_m\":2195,\"start_assumed\":false,\"history\":\"unknown\",\"max_sleep_m\":4999,\"max_sleep_day\":6,\"max_sleep_place\":\"High Col Camp\",\"max_high_m\":5243,\"max_gain_m\":579,\"m ...}",
 "page_repair": "[{\"after_day\":1,\"action\":\"stop\",\"place\":\"\",\"sleep\":9840,\"sleep_m\":3000},{\"after_day\":4,\"action\":\"rest\",\"place\":\"Upper Pastures\",\"sleep\":13500,\"sleep_m\":4115}]",
 "page_reads": "[{\"level\":\"fix\",\"code\":\"jump\",\"text\":\"Night 2 at Hanging Bridge sleeps 1,900 ft higher than the night before (10,800 ft) - above 3,000 m the common limit is 500 m (about 1,640 ft) a night.\"},{\"level\":\"fix\",\"code\":\"no_rest\",\"text\":\"Night 5 at Moraine Camp is the 4th climbing night in a row above 3,000 m - take a night that does not climb at least every fourth night.\"},\"... 4 more\"]",
 "page_verdict": "add_nights",
 "notes": "We only have 8 nights off work. Camping from night 4."
}

Reply:

{
 "task": "pace",
 "title": "Ridge Trek to the High Col",
 "verdict": "add_nights",
 "summary": "The plan already brings you back down well below the high point, descending from High Col Camp through Kettle Village to Trailhead town over the last two nights. Above 9,800 ft it still needs nights added: it climbs 1,900 ft in one night to Hanging Bridge, past the 1,640-ft limit, and runs five climbing nights in a row before reaching a sleeping high of 16,400 ft at High Col Camp. Splitting that climb and adding a second night at Upper Pastures fixes both issues; the revised plan runs 10 nights against the 8 nights you have off work.",
 "changes": [
  {
   "after_day": 1,
   "action": "stop",
   "place": "",
   "sleep_m": 3000,
   "why": "The night after Cedar Lodge currently climbs 1,900 ft to Hanging Bridge, past the 1,640-ft limit above 9,800 ft; this new night splits that climb into two smaller steps."
  },
  {
   "after_day": 4,
   "action": "rest",
   "place": "Upper Pastures",
   "sleep_m": 4115,
   "why": "Night 5 at Moraine Camp is currently the fourth climbing night in a row above 9,800 ft, and night 6 at High Col Camp the fifth; a second night at Upper Pastures breaks that run before the fourth night."
  }
 ],
 "watch": [
  {
   "sign": "Headache with nausea, tiredness, dizziness or poor sleep at Moraine Camp, High Col Camp or on the climb to the col",
   "act": "Stop climbing and rest at the same altitude until it clears; go down if it gets worse."
  },
  {
   "sign": "Confusion, stumbling or clumsy walking, or unusual drowsiness at High Col Camp or above",
   "act": "Go down at once, with help, and get medical care."
  },
  "..."
 ],
 "turnaround": [
  "If symptoms of altitude sickness appear before Moraine Camp (15,100 ft), stay at that altitude until they clear before continuing to High Col Camp.",
  "If there is not enough daylight at High Col Camp (16,400 ft) to cross the 17,200-ft col and start down, turn back toward Kettle Village rather than push on after dark.",
  "..."
 ],
 "ask": [
  "Does the trip price include the 2 extra nights this pace needs, or spare days if the group goes slower?",
  "What descent and evacuation options are there from Moraine Camp, High Col Camp and the 17,200-ft col if someone needs to go down?",
  "..."
 ],
 "caveats": [
  "Altitudes are as entered on this plan, in feet.",
  "..."
 ]
}

task: pace - Fly-in lake trek - rethink

Input:

{
 "task": "pace",
 "today": "2026-10-21",
 "trip": "Fly-in lake trek",
 "unit": "m",
 "itinerary": "Airstrip town | 3560 m\nLake village | 4050 m\nUpper lake | 4700 m\nPass camp | 5250 m\nLake village | 4050 m\nAirstrip town | 3560 m ... (128 characters)",
 "start": "{\"place\":\"Capital city\",\"alt\":1300,\"alt_m\":1300,\"assumed\":false}",
 "nights": "[{\"day\":1,\"place\":\"Airstrip town\",\"sleep\":3560,\"sleep_m\":3560,\"high\":null,\"high_m\":null,\"gain\":2260,\"gain_m\":2260,\"note\":\"\",\"flags\":[\"arrival\",\"big_jump\"]},{\"day\":2,\"place\":\"Lake village\",\"sleep\":4050,\"sleep_m\":4050,\"high\":null,\"high_m\":null,\"gain\":490,\"gain_m\":490,\"note\":\"\",\"flags\":[]},\"... 4 more\"]",
 "facts": "{\"unit\":\"m\",\"nights\":6,\"start_place\":\"Capital city\",\"start_m\":1300,\"start_assumed\":false,\"history\":\"mild\",\"max_sleep_m\":5250,\"max_sleep_day\":4,\"max_sleep_place\":\"Pass camp\",\"max_high_m\":5250,\"max_gain_m\":2260,\"max_gain_d ...}",
 "page_repair": "[{\"after_day\":0,\"action\":\"stop\",\"place\":\"\",\"sleep\":3000,\"sleep_m\":3000},{\"after_day\":0,\"action\":\"stop\",\"place\":\"\",\"sleep\":3280,\"sleep_m\":3280},\"... 4 more\"]",
 "page_reads": "[{\"level\":\"fix\",\"code\":\"arrival\",\"text\":\"Night 1 at Airstrip town is 3,560 m, 2,260 m above Capital city - flying or driving straight to 3,500 m (about 11,500 ft) or higher is a high-risk start that extra nights later cannot undo. Start lower, or spend nights on the way up.\"},{\"level\":\"fix\",\"code\":\"big_jump\",\"text\":\"Night 1 at Airstrip town sleeps 2,260 m higher than the night before (3,560 m) - above 3,000 m the common limit is 500 m (about 1,640 ft) a night. That is more than two nights' worth.\"},\"... 6 more\"]",
 "page_verdict": "rethink",
 "notes": "We fly from the city to the airstrip on the morning of night 1. I had a headache and nausea once at 3,000 m."
}

Reply:

{
 "task": "pace",
 "title": "Fly-In Lake Trek to Pass Camp",
 "verdict": "rethink",
 "summary": "The descent back down through Lake village and Airstrip town on the way out is sensible, but the plan still needs rethinking, not just extra nights, because flying straight in to a first night at 3,560 m is a high-risk start that nights added later cannot undo. From that start the plan also climbs 2,260 m onto a night above 3,000 m, runs four climbing nights in a row above 3,000 m, and tops out at 5,250 m. The single most useful change is a different start - a lower first night on the way up, or trekking in rather than flying in - with the climb up to Pass camp at 5,250 m spread out more once that start is fixed. Since you have had a headache and nausea before at 3,000 m, plan a pace slower than the minimum and talk with a doctor before you go.",
 "changes": [],
 "watch": [
  {
   "sign": "A headache along with nausea, tiredness, dizziness or poor sleep at Upper lake (4,700 m) or Pass camp (5,250 m)",
   "act": "Stay at that height until it clears, and go down if it gets worse."
  },
  {
   "sign": "Confusion, stumbling or clumsy walking, or unusual drowsiness at Upper lake or Pass camp",
   "act": "Go down at once, with help, and get medical care."
  },
  "..."
 ],
 "turnaround": [
  "If the only way in is still a flight straight to Airstrip town at 3,560 m, treat this plan as needing a different start - a lower first night or a trek in - before it is ready to book, not just extra nights stacked on afterward.",
  "If a headache, nausea, tiredness or poor sleep appears at Upper lake (4,700 m) or Pass camp (5,250 m), stop at that height until it clears before going higher.",
  "..."
 ],
 "ask": [
  "Are spare days built into the price if the pace before Pass camp needs to slow down?",
  "What are the descent and evacuation options from Upper lake and Pass camp if someone needs to come down quickly?",
  "..."
 ],
 "caveats": [
  "Altitudes are as entered in this plan.",
  "..."
 ]
}

Build the input yourself

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

const K = PaceKit;
const st = K.state({
  today: "2026-10-21", trip: "Ridge trek", unit: "ft", start_place: "Trailhead town", start_alt: "7200",
  history: "unknown", notes: "",
  days: "Cedar Lodge | 8,900 ft\nHanging Bridge | 10,800 ft\nKettle Village | 12,100 ft"
});
console.log(st.walk.rows, st.extras, st.verdict);
const { input } = K.buildInput(st, { today: "2026-10-21" });
const wire = K.toWire(input);           // POST this to /estimate and /run
// later: const reply = K.normalize(K.parseReply(job.output.output));
// const { revised, breaks, notes } = K.check(reply, st);

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://sleep-low.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":"sleep-low"}' | 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": "pace", "today": "2026-10-21", "unit": "ft", "itinerary": "Cedar Lodge | 8,900 ft ...", "start": "{...}", "nights": "[...]", "facts": "{...}", "page_repair": "[...]", "page_reads": "[...]", "page_verdict": "add_nights"}
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 sleep-low:pace:<inputHash>:a<attempt>, 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="sleep-low:pace:$(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: sleep-low:pace:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json