swamp model for reading Peloton workout data via the unofficial api.onepeloton.com API
  • TypeScript 100%
Find a file
keeb d38784b66c v2: Auth0 token auth + decomposed per-ride data model
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>
2026-07-06 14:29:22 -07:00
extensions/models v2: Auth0 token auth + decomposed per-ride data model 2026-07-06 14:29:22 -07:00
.env.example Initial commit: @keeb/peloton-rides model 2026-07-06 13:57:50 -07:00
.gitignore Add swamp repo scaffolding (swamp init) 2026-07-06 14:02:59 -07:00
CLAUDE.md v2: Auth0 token auth + decomposed per-ride data model 2026-07-06 14:29:22 -07:00
deno.json Initial commit: @keeb/peloton-rides model 2026-07-06 13:57:50 -07:00
LICENSE.txt Initial commit: @keeb/peloton-rides model 2026-07-06 13:57:50 -07:00
manifest.yaml v2: Auth0 token auth + decomposed per-ride data model 2026-07-06 14:29:22 -07:00
README.md v2: Auth0 token auth + decomposed per-ride data model 2026-07-06 14:29:22 -07:00

@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 (1100, 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 (1100, 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 the auth resource, 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.