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.
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.
| 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. |
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.
# 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=30swamp extension pull @keeb/peloton-ridesOr, for local development, add this repo as an extension source:
swamp extension source add /path/to/keeb-pelotondeno task check # type-check the model
deno task lint
deno task test # unit tests for the pure helpers
deno task fmtMIT — see LICENSE.txt.