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": {…}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or task is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the pace check is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. 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 |
|---|---|
rethink | Night 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_nights | Otherwise, 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_pace | No night breaks a rule. changes is empty unless the notes ask for a spare night. |
| Action | What the page does with it |
|---|---|
rest | A second night right after planned night after_day (0 = after the start), at that night's place and altitude. |
stop | A 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
| Field | Type | Notes |
|---|---|---|
task | string | Always pace. |
today | string | YYYY-MM-DD. |
trip | string | The trek's name (may be empty). |
unit | string | m or ft - the unit the itinerary is written in. |
itinerary | string | One 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. |
start | string (JSON) | {place, alt, alt_m, assumed} - where you sleep the night before night 1. |
nights | string (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. |
facts | string (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_repair | string (JSON) | [{after_day, action, place, sleep, sleep_m}] - the fewest nights that keep the plan to the rules. |
page_reads | string (JSON) | [{level, code, text}]; level is fix or info. |
page_verdict | string | The page's own reading: on_pace, add_nights or rethink. |
notes | string | Optional, your own comments; past 600 characters the middle is cut. |
retry_note | string | Only 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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://sleep-low.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// sleep-low.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://sleep-low.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://sleep-low.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// SleepLow.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class SleepLow {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://sleep-low.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://sleep-low.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://sleep-low.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://sleep-low.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "sleep-low"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "sleep-low" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "sleep-low"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "sleep-low"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "sleep-low" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "sleep-low"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "sleep-low" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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}'
inp = json.load(open("input.json")) # {"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"}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "pace", today, trip, unit, itinerary, start, nights, facts, page_repair, page_reads, page_verdict, notes?}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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}'
import hashlib, time
key = "sleep-low:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `sleep-low:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "sleep-low:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "sleep-low:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "sleep-low:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "sleep-low:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"sleep-low:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
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
rev = json.load(open("input.json")) # the same run input as input.json: {"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"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "sleep-low:pace:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `sleep-low:pace:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "sleep-low:pace:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "sleep-low:pace:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "sleep-low:pace:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: sleep-low:pace:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"sleep-low:pace:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);