← Daylight Snacks
Snack API
Tokens

Drive Daylight Snacks from your own code

The one metered task, plan, is available over HTTP. It takes a place, the date the plan starts, a weekday schedule in plain lines, a weekly goal, what you like doing outside, what gets in the way, and the page's own daylight numbers. It returns a menu of sun snacks (short daylight outings), a week of punches that fits the reachable windows, three backups for missed days, a note on the darkest week and a first step.

The division of labour first: the page owns the sun's times, the reachable windows, the darkest week, the punch count and the verdict rule; the model owns the menu and the wording; the page re-checks the reply. Build page_facts with the page's own script (see Build the input yourself) rather than by hand.

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":"daylight-snacks"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, place, plan_from, schedule, goal, likes, limits, page_facts) 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: a plan 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 and the verdicts

Every run sends task: plan (the one task is plan); a missing or different one is still answered as plan. The verdict is a rule on N, the daylight minutes reachable on a workday of the darkest week (the workday_total line of page_facts), checked in this order: days-off when N is under 20, squeeze from 20 to 59, plenty at 60 or more.

Input fields (every value is a string)

FieldRequiredWhat it holds
taskyesAlways plan.
placeyesThe place label, e.g. London, UK, or Latitude 52.37, longitude 4.9.
plan_fromyesThe first day of the plan, YYYY-MM-DD.
scheduleyesPlain lines: workdays and days off, getting up and bedtime, work hours, the commute (indoors, or outdoors on foot or by bike) and the lunch break.
goalyesSun snacks wanted per week, 3 to 21.
likes / limitsyesWhat you like doing outside and what gets in the way (at most 300 characters each), or (not given).
page_factsyesThe page's numbers, one key | ... per line: place, plan_from, this_week, darkest_week, optional clock_change, work_days, days_off, four workday_window lines (morning, commute, lunch, evening), workday_total, dayoff_window, snacks_that_fit and target_snacks.
clip_notenoWhat the page cut, if anything.
retry_notenoOnly on the automatic reformat retry.

The reply

One JSON object, in output.output of the finished job:

{"task": "plan", "title": "...", "verdict": "plenty | squeeze | days-off", "verdict_reason": "...",
 "snacks": [{"id": "s1", "name": "...", "minutes": 15, "window": "morning | commute | lunch | evening | dayoff", "how": "..."}],
 "week": [{"day": "mon", "snack": "s1"}],
 "backup": ["...", "...", "..."], "darkest_week": "...", "first_step": "..."}
FieldWhat it holds
verdict / verdict_reasonThe rule above, and one sentence (at most 30 words) quoting N and the windows that hold it.
snacks2 to 8 distinct ideas (never more than target_snacks; none when it is 0), each with a window that has at least 10 minutes, 10 to 60 minutes that fit the window, a name (at most 6 words) and how (at most 22).
weekExactly target_snacks punches in day order; workdays take workday windows (each at most once a day), days off take day-off snacks within the day's daylight, at most 3 a day, every snack used.
backupExactly three ideas (at most 18 words) for a day when every window is missed.
darkest_week / first_step / titleHow the darkest week differs, quoting both weeks' sun times (at most 35 words); one small first step (at most 20); a playful title (at most 8).

The page then checks the reply (DayKit.check): the verdict against the rule and the quoted minutes, every snack inside its window, workday and day-off windows on the right days, the punch count, three backups, the word limits, clock times that appear in the schedule or the sun's times, and no weekday, season, he or she, health-claim word or internal field name in the text. Daylight Snacks is a daylight planner, not medical advice.

Worked examples, one per verdict

These are the three free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover plenty, squeeze and days-off.

Lisbon, from home - plenty

Input:

{
 "task": "plan",
 "place": "Lisbon, Portugal",
 "plan_from": "2026-10-19",
 "schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 07:30, bed at 23:30\nWork 09:00-17:30\nNo commute\nLunch break 13:00 for 60 min",
 "goal": "12",
 "likes": "Coffee on the balcony, walking along the river, a bike ride on a free day",
 "limits": "Calls most afternoons from 15:00",
 "page_facts": "place | Lisbon, Portugal | latitude 38.72 | time zone Europe/Lisbon\nplan_from | 2026-10-19\nthis_week | 2026-10-19 | sunrise 07:49 | sunset 18:54 | daylight 11 h 05 min\ndarkest_week | 2027-01-04 | sunrise 07:55 | sunset 17:27 | daylight 9 h 33 min\nclock_change | week of 2026-10-26 | clocks go back 1 hour\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 65 min | 07:55-09:00\nworkday_window | commute | 0 min | no commute\nworkday_window | lunch | 60 min | 13:00-14:00\nworkday_window | evening | 0 min | in the dark\nworkday_total | 125 min\ndayoff_window | dayoff | 572 min | 07:55-17:27\nsnacks_that_fit | 16\ntarget_snacks | 12"
}

Reply:

{
 "task": "plan",
 "title": "Your Lisbon Daylight Snack Lineup",
 "verdict": "plenty",
 "verdict_reason": "125 minutes of workday daylight land in the morning and lunch windows, with none in the commute or evening.",
 "snacks": [
  {
   "id": "s1",
   "name": "Balcony Coffee In Daylight",
   "minutes": 20,
   "window": "morning",
   "how": "Bring your coffee out to the balcony while the early light is there."
  },
  {
   "id": "s2",
   "name": "River Walk Before Calls",
   "minutes": 45,
   "window": "lunch",
   "how": "Walk along the river during your lunch break, finishing before your afternoon calls begin."
  },
  {
   "id": "s3",
   "name": "Day Off Bike Ride",
   "minutes": 45,
   "window": "dayoff",
   "how": "Take a bike ride while the daylight lasts on your day off."
  }
 ],
 "week": [
  {
   "day": "mon",
   "snack": "s1"
  },
  {
   "day": "mon",
   "snack": "s2"
  },
  {
   "day": "tue",
   "snack": "s1"
  },
  {
   "day": "tue",
   "snack": "s2"
  },
  {
   "day": "wed",
   "snack": "s1"
  },
  {
   "day": "wed",
   "snack": "s2"
  },
  {
   "day": "thu",
   "snack": "s1"
  },
  {
   "day": "thu",
   "snack": "s2"
  },
  {
   "day": "fri",
   "snack": "s1"
  },
  {
   "day": "fri",
   "snack": "s2"
  },
  {
   "day": "sat",
   "snack": "s3"
  },
  {
   "day": "sun",
   "snack": "s3"
  }
 ],
 "backup": [
  "Sit near the brightest spot indoors for a few minutes to catch some daylight.",
  "Step outside the doorway for a short stretch to catch the last of the light.",
  "Add extra time on your next day off to make up the missed daylight."
 ],
 "darkest_week": "Sunrise shifts from 07:49 to 07:55 and sunset from 18:54 to 17:27, with clocks going back 1 hour the week of 2026-10-26.",
 "first_step": "On the first day, enjoy coffee on the balcony in the early light."
}

London, by train - squeeze

Input:

{
 "task": "plan",
 "place": "London, UK",
 "plan_from": "2026-10-26",
 "schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 06:45, bed at 23:00\nWork 09:00-17:30\nCommute 45 min each way, indoors (car, bus or train)\nLunch break 12:30 for 30 min",
 "goal": "10",
 "likes": "Walking, a park bench with a sandwich, the allotment on days off",
 "limits": "Two kids to get to school before the train",
 "page_facts": "place | London, UK | latitude 51.51 | time zone Europe/London\nplan_from | 2026-10-26\nthis_week | 2026-10-26 | sunrise 06:43 | sunset 16:46 | daylight 10 h 03 min\ndarkest_week | 2026-12-28 | sunrise 08:06 | sunset 15:58 | daylight 7 h 52 min\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 9 min | 08:06-08:15\nworkday_window | commute | 0 min | indoors\nworkday_window | lunch | 30 min | 12:30-13:00\nworkday_window | evening | 0 min | in the dark\nworkday_total | 39 min\ndayoff_window | dayoff | 472 min | 08:06-15:58\nsnacks_that_fit | 11\ntarget_snacks | 10"
}

Reply:

{
 "task": "plan",
 "title": "Your London Daylight, Snack by Snack",
 "verdict": "squeeze",
 "verdict_reason": "Your workday daylight comes to 39 minutes, all of it in the morning and lunch windows.",
 "snacks": [
  {
   "id": "s1",
   "name": "Lunch bench with a sandwich",
   "minutes": 15,
   "window": "lunch",
   "how": "Take your sandwich to a bench during your break and sit outside."
  },
  {
   "id": "s2",
   "name": "A short lunch walk",
   "minutes": 10,
   "window": "lunch",
   "how": "Use part of your break for a short, easy walk outside."
  },
  {
   "id": "s3",
   "name": "Time at the allotment",
   "minutes": 45,
   "window": "dayoff",
   "how": "Spend time at the allotment, for as long as suits you."
  },
  {
   "id": "s4",
   "name": "A longer walk outside",
   "minutes": 30,
   "window": "dayoff",
   "how": "Take a longer walk outside, wherever feels good, with no rush."
  },
  {
   "id": "s5",
   "name": "A few minutes standing outside",
   "minutes": 20,
   "window": "dayoff",
   "how": "Step outside and stand in the light for a few minutes, no agenda."
  }
 ],
 "week": [
  {
   "day": "mon",
   "snack": "s1"
  },
  {
   "day": "tue",
   "snack": "s2"
  },
  {
   "day": "wed",
   "snack": "s1"
  },
  {
   "day": "thu",
   "snack": "s2"
  },
  {
   "day": "fri",
   "snack": "s1"
  },
  {
   "day": "sat",
   "snack": "s3"
  },
  {
   "day": "sat",
   "snack": "s4"
  },
  {
   "day": "sat",
   "snack": "s5"
  },
  {
   "day": "sun",
   "snack": "s4"
  },
  {
   "day": "sun",
   "snack": "s3"
  }
 ],
 "backup": [
  "Sit by the brightest window you can find for a few minutes.",
  "Stand near an open door or window for a short stretch.",
  "Add the missed time to your next day off outside."
 ],
 "darkest_week": "This week the sun rises 06:43 and sets 16:46; in the darkest week it rises 08:06 and sets 15:58, giving noticeably less daylight.",
 "first_step": "On your first day, take your sandwich to a bench at lunch."
}

Helsinki, 08:00 to 16:30 - days-off

Input:

{
 "task": "plan",
 "place": "Helsinki, Finland",
 "plan_from": "2026-10-26",
 "schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 06:30, bed at 22:30\nWork 08:00-16:30\nCommute 30 min each way, indoors (car, bus or train)\nLunch break 11:30 for 15 min",
 "goal": "8",
 "likes": "Cross-country skiing when there is snow, a sauna evening",
 "limits": "Lunch is usually eaten at the desk",
 "page_facts": "place | Helsinki, Finland | latitude 60.17 | time zone Europe/Helsinki\nplan_from | 2026-10-26\nthis_week | 2026-10-26 | sunrise 07:27 | sunset 16:42 | daylight 9 h 15 min\ndarkest_week | 2026-12-21 | sunrise 09:24 | sunset 15:13 | daylight 5 h 49 min\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 0 min | in the dark\nworkday_window | commute | 0 min | indoors\nworkday_window | lunch | 15 min | 11:30-11:45\nworkday_window | evening | 0 min | in the dark\nworkday_total | 15 min\ndayoff_window | dayoff | 349 min | 09:24-15:13\nsnacks_that_fit | 11\ntarget_snacks | 8"
}

Reply:

{
 "task": "plan",
 "title": "Chasing Helsinki's Last Light",
 "verdict": "days-off",
 "verdict_reason": "Your lunch window holds all 15 minutes of workday daylight; morning, commute and evening hold none.",
 "snacks": [
  {
   "id": "s1",
   "name": "Quick Midday Fresh-Air Break",
   "minutes": 10,
   "window": "lunch",
   "how": "Step outside for a few minutes before eating lunch at your desk."
  },
  {
   "id": "s2",
   "name": "Cross-Country Ski Loop",
   "minutes": 60,
   "window": "dayoff",
   "how": "Head out for a cross-country ski loop if there is snow."
  },
  {
   "id": "s3",
   "name": "Slow Outdoor Loop",
   "minutes": 45,
   "window": "dayoff",
   "how": "Take a slow loop outside, breathing in the daylight at your own pace."
  },
  {
   "id": "s4",
   "name": "Walk Then Sauna Evening",
   "minutes": 30,
   "window": "dayoff",
   "how": "Take a walk in the daylight, then settle into a sauna evening."
  }
 ],
 "week": [
  {
   "day": "mon",
   "snack": "s1"
  },
  {
   "day": "tue",
   "snack": "s1"
  },
  {
   "day": "wed",
   "snack": "s1"
  },
  {
   "day": "thu",
   "snack": "s1"
  },
  {
   "day": "fri",
   "snack": "s1"
  },
  {
   "day": "sat",
   "snack": "s2"
  },
  {
   "day": "sat",
   "snack": "s3"
  },
  {
   "day": "sun",
   "snack": "s4"
  }
 ],
 "backup": [
  "Stand outside for a few minutes at some point today.",
  "Spend a few minutes near natural light indoors instead.",
  "Add the missed time to your next day off outing."
 ],
 "darkest_week": "By the darkest week, sunrise shifts from 07:27 to 09:24 and sunset moves from 16:42 to 15:13, narrowing the daylight further.",
 "first_step": "On your first day, step outside briefly during lunch before eating at your desk."
}

Build the input yourself

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

const K = DayKit;
const built = K.buildInput({ placeId: "london", start: "2026-10-26", workDays: ["mon", "tue", "wed", "thu", "fri"],
  wake: "06:45", bed: "23:00", workStart: "09:00", workEnd: "17:30", commute: "45", commuteMode: "indoor",
  lunchStart: "12:30", lunchMin: "30", goal: "10", likes: "Walking" });
built.verdict;                                   // "squeeze" - 39 reachable minutes on a workday of the darkest week
built.weeks[built.dark].date;                    // "2026-12-28"
const wire = K.toWire(built.input);              // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes                   // the page's re-check of the plan
// K.ics(built, reply)                           // the punches as weekly calendar events
// Custom place: { placeId: "custom", lat: "52.37", lon: "4.9", tz: "Europe/Amsterdam", ... }

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://daylight-snacks.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":"daylight-snacks"}' | 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": "plan", "place": "London, UK", "plan_from": "2026-10-26", "schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 06:45, bed at 23:00\nWork 09:00-17:30\nCommute 45 min each way, indoors (car, bus or train)\nLunch break 12:30 for 30 min", "goal": "10", "likes": "Walking, a park bench with a sandwich, the allotment on days off", "limits": "Two kids to get to school before the train", "page_facts": "place | London, UK | latitude 51.51 | time zone Europe/London\nplan_from | 2026-10-26\nthis_week | 2026-10-26 | sunrise 06:43 | sunset 16:46 | daylight 10 h 03 min\ndarkest_week | 2026-12-28 | sunrise 08:06 | sunset 15:58 | daylight 7 h 52 min\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 9 min | 08:06-08:15\nworkday_window | commute | 0 min | indoors\nworkday_window | lunch | 30 min | 12:30-13:00\nworkday_window | evening | 0 min | in the dark\nworkday_total | 39 min\ndayoff_window | dayoff | 472 min | 08:06-15:58\nsnacks_that_fit | 11\ntarget_snacks | 10"}
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 daylight-snacks:<task>:<inputHash>:a<attempt> (the one task is plan), 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="daylight-snacks:plan:$(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: daylight-snacks:plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json