API
Base URL https://watchout.run — all endpoints are GET, unauthenticated,
and deterministic: the same parameters always produce the same bytes and the same workout UUID,
so re-importing never duplicates on the watch. Machine-readable spec: /openapi.json.
Workout parameters
Shared by /gen, /w and /ics. Pace is always min:sec per km.
| param | example | meaning |
|---|---|---|
name | Threshold 3x2km | Workout name |
d | 260811 | Date YYMMDD, prefixed to title and filename (makes it searchable on the watch) |
wu | 15@5:45 | Warmup goal, optional: 15@5:45 (minutes @ pace), 2000m@5:45 (distance), open (no target), or none to skip it. HR range in place of pace: 12@h120-140. Omit the param for a default warmup. |
cd | 10@6:00 | Cooldown goal, optional: same forms — 10@6:00 · 1500m@6:00 · open · none |
b | 3x2000@4:25-4:20,r120 | Interval block, repeatable. repsxsteps, steps comma-separated. Work: 2000@4:25-4:20 (pace range) · 400@3:55 (single pace) · 2000@h140-155 (HR range bpm) · t30/t300@4:25 (timed, seconds) · 2000 (no alert) · open (until you tap) · open~4000 (open, expected ~4 km — display and totals only, the step still ends on a tap). Rest (r-prefix): r120 (seconds) · r400m@6:00 (distance) · ropen. Any step takes an optional @pace or @h<lo>-<hi> |
uid | 9B2F… | Workout UUID override; omitted = derived deterministically from the parameters |
GET /gen
Returns the .workout file (Apple WorkoutKit protobuf, application/octet-stream).
On iPhone: download → open in the Fitness app → “Add to Apple Watch”.
/gen?name=Threshold+3x2km&d=260811&wu=15@5:45&cd=10@6:00&b=3x2000@4:25-4:20,r120
GET /fit
Same parameters (minus uid), returns a Garmin FIT workout file
with the identical step structure — speed ranges in mm/s, heart-rate ranges in bpm. Delivery:
Garmin Connect cannot import workout files, so upload it as a planned workout on
intervals.icu (free; auto-syncs
to the watch once Garmin is linked) or copy it to the watch's GARMIN/NewFiles folder over USB.
/fit?name=Threshold+3x2km&d=260811&wu=15@5:45&cd=10@6:00&b=3x2000@4:25-4:20,r120
GET /icu
The same spec as an intervals.icu
planned-workout event (JSON): workout_text in intervals.icu's own builder syntax plus a ready
event body. POST it as a one-element array to
https://intervals.icu/api/v1/athlete/0/events/bulk?upsert=true
(HTTP Basic, username API_KEY, password = your intervals.icu API key) and it appears on your
calendar and syncs to a linked Garmin as a native structured workout — this is what the workout page's
Send to Garmin button does, with the key kept in your browser. upsert=true matches
on external_id (the deterministic workout UUID), so re-sending the same session updates the
event instead of duplicating. Optional lthr=<bpm>
(your threshold HR) converts h<lo>-<hi> bpm steps to the % LTHR ranges
intervals.icu requires; open steps become 1s press lap (the watch waits for the lap button).
/icu?name=Threshold+3x2km&d=260811&wu=15@5:45&cd=10@6:00&b=3x2000@4:25-4:20,r120
GET /route.gpx
?name=…&p=<encoded polyline> (precision 5, e.g. Strava's map.summary_polyline)
returns a GPX course. Pass the same polyline as route= on /w or /ics and the workout page
renders the route shape with the GPX link (the calendar event's page link carries it). Garmin Connect imports GPX as a
Course (followable on the watch); Apple users open it in their maps/running app.
GET /w
Human-friendly workout page with “Add to Apple Watch”, “Send to Garmin watch” (via intervals.icu, one-time API-key setup kept in the browser) and “Add to Calendar” buttons — the link to share. Extra optional parameters:
| param | example | meaning |
|---|---|---|
dt | 20260811T1945 | Start time YYYYMMDDTHHMM, local to tz. Enables the calendar button; d is derived if omitted |
tz | Europe/London | IANA timezone for the calendar event (default Europe/Oslo) |
e | 🔥 | Session-type emoji: 🔥 intervals/threshold · 🟢 easy · 🛣️ long · ⚡ hills/fartlek · 🏁 race |
dur | 75 | Calendar event duration in minutes (default: the session's estimated time, rounded up to the nearest 30 min) |
wx | 14°C, 3 m/s SW, dry | Forecast one-liner for the start hour — shown 🌤 on the page and in the calendar event (≤160 chars, display-only) |
note | Keep the reps controlled | Coach's note to the runner — shown 📝 on the page and in the calendar event (≤500 chars, display-only) |
route | <polyline5> | Encoded route polyline behind a compact reveal: terrain-painted map shape (grade colors, elevations fetched in the browser from Open-Meteo) + GPX course link |
rnew | 1 | Marks a route the runner doesn't know yet — the map opens expanded instead of collapsed. Display-only |
fx | 2000,2000@3,333,2000@-2 | Post-run effort coloring — the route map by deviation vs plan, plus a band under the elevation profile: sequential <meters>[@<±s/km> vs plan] segments, one per lap (positive = slower; no @ = no target). Display-only |
/w?name=Threshold+3x2km&d=260811&wu=15@5:45&cd=10@6:00&b=3x2000@4:25-4:20,r120&dt=20260811T1945&e=🔥
GET /ics
Calendar event (text/calendar) for the workout; takes the same parameters as
/w, dt required. The event's URL field links to the workout page; the
description is the runner's briefing — estimated distance/time, the forecast
(wx=), the steps one per line, and the coach's note (note=). No links in
the description (the URL field carries the page); cal=upsert adds one, since
Google Calendar drops the URL property on import. Opening the .ics lets the runner
choose which of their calendars the event
is added to; the workout page also offers a Google Calendar link. The event's
UID is the workout UUID (honours uid=) plus a seq= revision suffix.
The event's link IS the workout, and iOS ignores re-imports of a known UID
(verified iOS 26) — so a retuned or moved session bumps seq and imports as
a new event, marked (rev N) in the title, carrying the tuned link;
the runner deletes the superseded event. Re-importing the same revision is deduped.
Google Calendar does the opposite and updates a known UID on import:
cal=upsert keeps the UID bare across revisions (no rev marker), so there a
retune replaces the event in place. The watch file and the intervals.icu event update
in place on the same uid throughout.
/ics?name=Threshold+3x2km&d=260811&wu=15@5:45&cd=10@6:00&b=3x2000@4:25-4:20,r120&dt=20260811T1945&dur=60&e=🔥
GET /plan & /plan.ics
A whole training plan as one link. plan= is a stable plan id,
each s= is one session's url-encoded parameter string (same params as
/ics), in slot order. /plan renders the sessions with an
Add-all-to-Calendar button (/plan.ics — one VEVENT per session) and a
Send-all-to-Garmin button (one intervals.icu bulk POST from the browser).
Session identity is plan id + slot index, never the date: a retuned or
rescheduled session keeps its UUID/external_id, so the watch replaces and
intervals.icu upserts. Calendar events are revision-scoped (uid + seq
inside each s=): iOS ignores re-imports of a known UID, so a tuned or
moved session bumps its seq and imports as a new event carrying the tuned
link — the runner deletes the superseded event (the new one is marked
(rev N)). Unchanged sessions dedupe on re-import, so re-importing a
re-emitted plan adds only what changed. For Google Calendar — which
updates a known UID on import — add cal=upsert to
/plan.ics: UIDs stay bare and a re-import replaces retuned events in
place; the plan page offers this file behind its Google Calendar link.
/plan?plan=oslo-hm-2026w29&s=name%3DThreshold%26dt%3D20260714T1800%26b%3D3x2000%404%3A25%2Cr120&s=…
POST /mcp
Stateless MCP server (Streamable HTTP, CORS-enabled, no auth). Add
https://mcp.watchout.run as a custom connector in Claude
(Settings → Connectors → Add custom connector) or ChatGPT (Settings →
Apps & Connectors → Developer mode → Create; paid plans;
download the icon for the optional PNG-upload
field — 256×256, 2.6 KB) — any MCP client
speaking Streamable HTTP works. It works best when the runner's own Strava is
connected in the same agent (or their
training history lives in the project) — the interview and design guidance
read routes and paces from there; this server stores nothing. Tools:
make_workout_links (build a workout, get page/file/calendar links),
make_plan_links (a whole training plan in one call: plan page, one multi-event .ics, bulk Garmin send, per-slot manifest),
show_plan_progress (the plan's at-a-glance dashboard: the agent passes its manifest + what happened, the server computes done/missed, km and adherence, the inline card draws it),
read_workout_link (decode any pasted WatchOut link back into session state — links carry the full session, so "refresh my calendar event" needs nothing but the link),
now (the server clock, with local wall-time in a given timezone — fresh chats anchor "today" to it),
whats_new (changelog since the version that generated a saved plan/workout — material changes come with what-to-do notes; see /changelog),
runner_profile_interview (one-time "grill me" setup: mine the runner's
Strava history for routes/laps/hills/paces and save a profile in the agent's own memory),
workout_design_guide (course-aware design method: lap math, floats, hills),
check_session (deterministic session math: lap fit, suggested floats, totals — agents verify before linking),
check_route (route ground truth: a polyline in — real distance, elevation gain, per-km grades and the flat stretches fit for reps out; for places the runner doesn't know),
get_weather (hourly forecast for the route via MET Norway / yr.no — heat/wind/cold thresholds for pace-vs-HR decisions),
tune_session_guide (frictionless pre-run retuning; workout_uuid reuse replaces the session on the watch),
adapt_plan_guide (the arc, not the session: missed weeks, illness, travel, a moved race, and the taper table),
check_plan (the shape of the WEEKS: ramp rate, long-run share, hard-day spacing, down weeks),
pace_zones (Daniels training bands from a real performance, each ready to paste into a step),
review_session_guide (post-run lap-vs-plan debrief, profile updates, Strava retitling via the runner's own Strava MCP),
garmin_delivery_guide (the ranked paths onto a Garmin watch — intervals.icu sync first),
log_to_strava (one-tap post-run Strava update via the runner's own browser — with the run's effort-vs-plan route visual when the session had one) and
get_strava_activities (read-Strava handoff for hosts without Strava tools — the runner fetches their own runs client-side),
pull_strava_routes (saved-routes round-trip: the runner sends their routes from the browser, encrypted end-to-end; the inline card hands them to the agent),
fetch_strava_handoff (collect that one-shot encrypted drop)
and workout_format_help (full format documentation). The
setup_runner MCP prompt exposes the interview as a one-click action
in clients that support prompts.
curl -X POST https://mcp.watchout.run/ -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"make_workout_links",
"arguments":{"name":"Threshold 3x2km","date":"260811","emoji":"🔥","warmup":"15@5:45",
"cooldown":"10@6:00","blocks":["3x2000@4:25-4:20,r120"],"start":"20260811T1945"}}}'
Addresses
mcp.watchout.run serves the MCP endpoint on / — it is the address to paste
into Claude or ChatGPT. Everything else lives on watchout.run: /apple and /garmin preset
the landing platform, and ?dev=garmin does the same on workout pages — share
watchout.run/w?…&dev=garmin with a Garmin friend and they land on the right button.