- TypeScript 100%
Peloton retired POST /auth/login (403 "Endpoint no longer accepting requests") and moved to Auth0. Authenticate with an Auth0 Bearer access token instead: - Preferred: vault-stored refresh token, exchanged for an access token and cached in the `auth` resource; only refreshes when the ~48h token expires, persisting each rotated refresh token forward (set once, self-renewing). - Fallback: a short-lived accessToken supplied directly. Decompose the data model (per keeb's review): each workout is its own addressable `ride` resource keyed by workout id — no monolithic list blob. getRecentRides is a factory (one ride per workout, preserving previously fetched detail); getRideDetail read-modify-writes the metrics into the same ride; getStats is a derived aggregate rollup. Fixes an instance-name collision (ride vs rideDetail shared the workoutId key) and the maxes source (per-metric max_value, not the averages array). Verified end-to-end live against a real Peloton account. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| extensions/models | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| deno.json | ||
| LICENSE.txt | ||
| manifest.yaml | ||
| README.md | ||
@keeb/peloton-rides
A swamp model for reading your Peloton workout history and per-ride metrics.
Peloton has no official public API, so this talks to the unofficial JSON
endpoints (api.onepeloton.com) the Peloton web app uses. It's read-only — it
never writes anything back to Peloton.
⚠️ Unofficial and unsupported. Peloton can change or break these endpoints at any time (they already retired the old password-login endpoint — see Auth below). Automated access is a gray area under Peloton's ToS; use it for your own data.
Data model
Each workout is its own addressable ride resource, keyed by workout id —
there is no monolithic list blob. Query them with swamp's data layer, not by
parsing an array:
swamp data list my-peloton
swamp data query 'modelName == "my-peloton" && dataType == "resource" &&
attributes.discipline == "cycling" && attributes.totalOutputKj > 80' \
--select 'attributes.startTime + " " + attributes.title'
Or in CEL from another model/workflow: data.findBySpec("my-peloton", "ride").
A ride's detail (metrics) is null until you run getRideDetail, which
enriches that same ride in place.
Methods
| Method | Args | What it does |
|---|---|---|
getRecentRides |
limit (1–100, default 20) |
Fetch recent workouts, write one ride resource each (keyed by workout id). Preserves already-fetched detail. |
getRideDetail |
workoutId, everyN (def 5) |
Enrich one ride in place: total/avg/max output, cadence, resistance, speed, HR, distance, calories, downsampled time series. |
getStats |
limit (1–100, default 30) |
Derived aggregate rollup (single stats resource): total minutes, total/avg output, PR count, discipline breakdown. |
Auth
Peloton retired username/password login; the web app now uses Auth0, and the
API is authenticated with an Auth0 Bearer access token (audience
https://api.onepeloton.com/). Two ways to supply credentials (from a vault,
never hardcoded):
refreshToken(preferred, self-renewing). The model exchanges it for an access token, caches that token (~48h) in theauthresource, and only spends the refresh token when the cache expires. Peloton rotates refresh tokens, so each rotated token is persisted forward — set it once and it renews itself.accessToken(simple fallback). A short-lived (~48h) token used directly; re-grab it when it expires.
Grab either from a logged-in browser: DevTools → Application/Storage → Local
Storage → members.onepeloton.com → the @@auth0spajs@@::... entry holds
body.access_token and body.refresh_token.
Setup
# 1. Store a credential in a vault
swamp vault create local_encryption peloton-creds
swamp vault put peloton-creds PELOTON_REFRESH_TOKEN # preferred
# (or PELOTON_ACCESS_TOKEN for the simple fallback)
# 2. Create a model instance wired to the vault
swamp model create @keeb/peloton-rides my-peloton \
--global-arg 'refreshToken=${{ vault.get(peloton-creds, PELOTON_REFRESH_TOKEN) }}'
# 3. Use it
swamp model method run my-peloton getRecentRides --input limit=10
swamp model method run my-peloton getRideDetail --input workoutId=<id>
swamp model method run my-peloton getStats --input limit=30
Install
swamp extension pull @keeb/peloton-rides
Or, for local development, add this repo as an extension source:
swamp extension source add /path/to/keeb-peloton
Development
deno task check # type-check the model
deno task lint
deno task test # unit tests for the pure helpers
deno task fmt
License
MIT — see LICENSE.txt.