Public read-only JSON API exposing your full Strava activity history. Designed to be consumed by an AI running coach. Runs on Cloudflare Workers + D1 + Hono.
next_url — an absolute HTTPS URL pointing back at the same endpoint with a fresh cache-bust token. Useful for clients that cache by URL (e.g. Claude's web_fetch)./api/v2/activities, /api/v2/summary) also include next_page_url — same shape as next_url but with the page incremented. It is null on the last page.cb-{unix_ms}-{4_hex} (e.g. cb-1729459200123-a4f2). Ignored server-side.Pagination, limits, and the cache-bust token live in the URL path. This is for HTTP clients that can only follow links found as paths (e.g. Claude's web_fetch, which won't follow URLs with query strings unless they appear verbatim in user input). All /n/..., /p/.../..., and /cb-... segments are optional and order-fixed.
/api/v2/activities[/p/{page}/{perPage}][/cb-{token}]List all activities (full history, no time cap), newest first. Defaults: page=1, perPage=50 (max 200). Returns next_url and next_page_url. Query ?type=Run still works.
/api/v2/activities/latest[/n/{limit}][/cb-{token}]Latest activities (limit 1-20, default 5) with full detail + downsampled 5s streams for workouts.
/api/v2/activities/{id}[/cb-{token}]Full detail for one activity. Slimmed: omits map, best_efforts, segment_efforts, and splits_standard (use splits_metric).
/api/v2/activities/{id}/streams[/cb-{token}]High-resolution time-series streams for one activity.
/api/v2/athlete[/cb-{token}]Authenticated athlete profile.
/api/v2/activities/search/q/{query}[/p/{page}/{perPage}][/cb-{token}]Search cached activities by case-insensitive substring match against name, type, and sport_type. Same response shape as /api/v2/activities (slim list + activity_{id} top-level URLs + next_page_url). Defaults: page=1, perPage=50 (max 200). Also supports ?q=&page=&per_page= as query string.
/api/v2/summary[/p/{page}/{perPage}][/cb-{token}]Aggregated training summary. Weeks are always returned newest→oldest. Without /p/... returns full history. With /p/{page}/{perPage} returns one page (default perPage=26 ≈ 6 months, max 520) plus next_page_url. totals_by_type and total_activities are always full-history.
GET /api/v2/activities/p/1/50
{
"next_url": "https://coach.christofferlarsson.se/api/v2/activities/p/1/50/cb-1729459200123-a4f2",
"next_page_url": "https://coach.christofferlarsson.se/api/v2/activities/p/2/50/cb-1729459200456-9b1c",
"page": 1,
"per_page": 50,
"count": 50,
"activity_18234782549": "https://coach.christofferlarsson.se/api/v2/activities/18234782549/cb-1729459200789-1a2b",
"activity_18221202225": "https://coach.christofferlarsson.se/api/v2/activities/18221202225/cb-1729459200790-3c4d",
"...": "one activity_{id} field per activity on the page (placed BEFORE activities[])",
"activities": [ { "id": 18234782549, ... }, ... ]
}
Slim list payload. The list response only returns the fields useful for scanning history: id, name, type, sport_type, start_date, description, distance, moving_time, elapsed_time, total_elevation_gain, average_speed, average_heartrate, max_heartrate, average_cadence, average_watts, suffer_score, workout_type, plus url and streams_url. Heavy or rarely-needed fields (notably map.summary_polyline, lat/lng, splits, laps, segment efforts, social counts, gear) are stripped to keep responses small enough that web_fetch doesn't truncate. Fetch /api/v2/activities/{id} for the full detail.
Top-level activity URL fields (experimental). The paged list response (/api/v2/activities[/p/...]) also includes one extra top-level field per activity on the page, named activity_{id} (e.g. activity_18234782549), whose value is a direct URL to that activity's detail endpoint with a fresh cb-token. These fields are placed before the activities array so they survive even if the array gets truncated by HTTP clients. Tokens regenerate per request. Same data is also available inside activities[i].url.
Activity detail is cached for 24h and streams are cached indefinitely. The summary list is re-synced at most once per 5 min. If you edited an activity on Strava (rename, change type, fix HR, add description, etc.) and want the API to pick it up immediately, hit one of these endpoints. All accept GET so Claude's web_fetch works.
/api/v2/sync/refresh[/cb-{token}]Cheap: forces a re-sync of the activity list past the 5-min throttle. Refreshes summary fields (name, type, distance) for any activity Strava returns. Does NOT clear detail/streams caches.
/api/v2/activities/{id}/refresh[/cb-{token}]Wipes detail + streams cache for one activity, then immediately re-fetches detail from Strava. Returns the fresh activity payload. Streams are re-fetched lazily on next /streams call.
/api/v2/activities/latest/refresh[/n/{count}][/cb-{token}]Forces a re-sync, then wipes detail + streams caches for the latest N activities (default 10, max 50). Re-fetched lazily on next read. Returns the cleared ids.
Strava's read API allows ~100 calls per 15 min. Use /sync/refresh when you only need updated names/types — it costs 1–2 calls. Use /{id}/refresh for a specific activity (1 call). Use /latest/refresh sparingly (N ≤ 30) to avoid burning the budget.
Reads the upcoming training plan from a Google Sheet (via service account). Cached for 10 minutes in D1, with stale-fallback if Sheets is unreachable.
/api/v2/schedule/upcoming[/cb-{token}]Next 14 days from the training plan. Returns days[] with weekday, sessionType, distanceKm, note, isQuality, isToday, isPast.
/api/v2/schedule/ical/{stable_path}.icsiCal feed (text/calendar) with the next 28 days. Subscribe in Google/Apple Calendar. Stable URL — do not change.
A mobile-friendly server-rendered view of today + the next 13 days is available at /schema.
Read and write cells in a Google Sheet via a service account. To use: enable the Google Sheets API in your Google Cloud project, create a service account + JSON key, store it as the GOOGLE_SERVICE_ACCOUNT_JSON Worker secret, and share the sheet with the service account's client_email (Editor for write access, Viewer for read-only).
/api/v2/sheets/list/{spreadsheet_id}[/cb-{token}]List tabs in a spreadsheet. Returns title + sheets[] with sheetId, title, index, rowCount, columnCount.
/api/v2/sheets/read/{spreadsheet_id}[/cb-{token}]?range={A1}Read a range in A1 notation, e.g. ?range=Sheet1!A1:E10 (URL-encode the range). Returns {range, majorDimension, values}.
/api/v2/sheets/update/{spreadsheet_id}[/cb-{token}]Batch-update ranges. Body: {"value_input_option":"USER_ENTERED","updates":[{"range":"Sheet1!A1:B1","values":[["a","b"]]}]}. value_input_option defaults to USER_ENTERED. Invalidates the schedule cache on success.
Access-Control-Allow-Origin: *.{ "error": string, "status": number }.web_fetch.