Skip to content

Garmin Connect changelog

io.github.dynamicsninja/garminconnect

5 changes recorded. MCP Harbor re-reads this listing from the official registry every six hours and asks the live server what it exposes, and keeps what changed between one look and the next.

  • server.json 2 fields changed
    icon_url
    — https://garmin.ficdev.xyz/mcp/icon.svg
    version
    0.11.0 0.11.1

  • server.json 1 field changed
    version
    0.10.0 0.11.0

  • AGENTS.md +41 −96

    https://raw.githubusercontent.com/dynamicsninja/garminconnect-js/HEAD/AGENTS.md

    − | `getActivity`                      | `(activityId: number \| string): Promise<Activity>`                                                                                                                                                                                                                                                                                                                                                                                                                          − `ApiOptions`: `{ method?, params?: Record<string,string|number|undefined>, json?: unknown, headers?: Record<string,string>, timeoutMs?: number }`. There is no public `retry` field on `ApiOptions` — see section 6 for what that means for POST.− `Garmin` has **193 methods**, every one listed in section 3. If a method is not in that table,−   valid and is rejected with `400 Invalid value 'TIME' for usageType`; the duration option is−   spelled `DURATION`.− - **Creating a goal is `POST /goal-service/goal/goal` — SINGULAR.** Not wrapped by this library.−   The read path is plural (`/goal/goals`), and POSTing to the plural form returns−   **405**, which is how this was found. Captured from Garmin's own UI; body shape:−   `{name, activityType, period, type, caloriesInKiloCalories, distanceInMeters, durationInSeconds,−   numberOfActivities, startDate, privacy}` — e.g. `period: "thirty_days"`,−   `type: "distance_accumulation"`, `privacy: "public"`. Reachable today via `connectapi`.− - **Training plans: BOTH endpoints are verified, and the "needs a PHASED plan" rule was wrong.**−   `trainingPlanCategory` has at least three values, not two. A Garmin Coach plan is `STATIC`;−   enrolling a plan from Training & Planning -> Training Plans produced `ITP`; `PHASED` has never−   actually been seen on an account. Against the ITP plan, `getTrainingPlanById` (`/phased/{id}`,−   36 keys) and `getAdaptiveTrainingPlanById` (`/fbt-adaptive/{id}`, 35 keys) BOTH succeed.−   So `/phased/{id}` is not restricted to category `PHASED` — it rejects `STATIC` and accepts `ITP`,−   and its 400 `"Not a phased plan."` describes the PATH, not a required category. The earlier−   reading inverted that, and the probe written from it filtered for `PHASED` and would have−   skipped forever on an account that could already answer the question. A probe that only looks−   for what you expect cannot show you that you expected the wrong thing; `npm run smoke:gaps` now−   tries every enrolled plan against both paths and prints the category alongside each result.−   **The plan id field is `trainingPlanId`** — not `planId`, not `id`.−   `/trainingplan-service/trainingplan/plans` returns ONLY the account's own enrolled plans (probed−   with and without owner and paging params), so a plan must be enrolled through the UI before−   either endpoint can be exercised at all.− - **There is no walking or hiking workout sport type, and the two helpers that pretended otherwise−   were REMOVED on 2026-09-24.** They sent `sportTypeId` 17 and 18, which look like Garmin−   ACTIVITY-type ids mistaken for workout sport types. Garmin's own enum−   (`GET /workout-service/workout/types` -> `workoutSportTypes`) contains only−   1,2,3,4,5,6,7,8,9,10,11,13. The POST is accepted, the workout is created, and the stored value−   is `{sportTypeId: 0, sportTypeKey: null, displayOrder: 0}` — confirmed live by creating one with−   each helper and reading it back. A method whose only possible outcome is a broken record is−   worse than no method. `WORKOUT_SPORT_TYPE_ID` does not carry 17/18 either. **For a walk or hike−   template, call `uploadWorkout` with `OTHER` (3) or `CARDIO_TRAINING` (6).**−   `tests/services/workouts.test.ts` asserts the ids stay out of the map and the helpers stay off−   the prototype. This was also the case where "the write returned 2xx" had been recorded as−   verification; only the read-back exposed it.− - **`GET /workout-service/workout/types` returns every workout enum Garmin uses** — nine of them:−   `workoutSportTypes`, `workoutStepTypes`, `workoutConditionTypes` (24), `workoutTargetTypes` (27),+ - **`getActivity` returns `ActivitySummary`, NOT the list-row `Activity`** (since 0.10.0):+   measurements are under `summaryDTO`, the type under `activityTypeDTO`. Many response types+   extend interfaces generated from Garmin's official Personal API spec (`PersonalApi`, section 3,+   "Types from Garmin's Personal API spec"); every field there is optional.+ - **`searchActivities` fails SILENTLY on bad names**: an unknown `sortBy` or `requiredFields`+   value returns `[]`, not an error, and Garmin's field names are not always the response's+   (`averageHR` matches nothing). List filters are sent as repeated parameters; never comma-join.+ | `getSpo2Acclimation` | `(cdate: string \| Date): Promise<Spo2Acclimation \| null>` — GETs `/wellness-service/wellness/daily/spo2acclimation/{cdate}` (Garmin's Personal API spec): the day's SpO2 averages, sleep and hourly series and the monitoring environment, typed from `PersonalApi.DailySpO2AndAcclimationDTO`. Distinct from `getSpo2Data`'s path. A day with no data (even 2030-01-01) still returns the object, fields `null` | yes, 2026-10-23, read-only on a real account (`smoke:real`); all 28 sp+ | `getDailySummaries` | `(start: string \| Date, end: string \| Date): Promise<DailySummary[]>` — GETs `/usersummary-service/v2/dailysummaries?startDate&endDate` and unwraps `data`: one object per day grouped by topic (`movement`, `calories`, `heartRate`, `intensityMinutes`, `stress`, `bodyBattery`, `hydration`, `respiration`, `pulseOx`, `jetLag`), typed from `PersonalApi.UserDailySummaryV2DTO`. Garmin caps one request at a year ("must be P1Y or less": a 365-day span passes, 366 is a 400), so lo+ | `getDailyStress` | `(start: string \| Date, end: string \| Date): Promise<DailyStressEntry[]>` — GETs `/usersummary-service/stats/stress/daily/{start}/{end}`: `{calendarDate, values: {overallStressLevel, restStressDuration, lowStressDuration, mediumStressDuration, highStressDuration}}` per day, durations in seconds. Garmin 400s `end - start > 27` ("cannot be more than 28 days"), so longer ranges go in 28-day windows; a reversed range throws `GarminError` before any request | yes, 2026-10-23, r+ | `getDailyHydration` | `(start: string \| Date, end: string \| Date): Promise<DailyHydrationEntry[]>` — GETs `/usersummary-service/stats/hydration/daily/{start}/{end}`: `{calendarDate, valueInML, goalInML}` per day that has an entry (the spec's `sweatLossInML`/`activityIntakeInML` were not seen). Garmin 400s `end - start > 30` ("cannot be more than 31 days"), so longer ranges go in 31-day windows | yes, 2026-10-23: three rows read back read-only from the test account's seeded hydration; `[]` on+ | `getActivity`                      | `(activityId: number \| string): Promise<ActivitySummary>` — NOT the list-row `Activity` shape: measurements are under `summaryDTO` (meters, seconds), the type under `activityTypeDTO`. `ActivitySummary` extends `PersonalApi.ActivitySummaryDTO`, typed from Garmin's Personal API spec (AGENTS.md section 3) | yes                                                                                                                                                       + | `searchActivities` | `(filters?: ActivitySearchFilters): Promise<Activity[]>` — one page from `/activitylist-service/activities/search/activities` (the `getActivities` endpoint) with the filters Garmin's Personal API spec documents: `search`, `activityType` (a PARENT key, includes its subtypes; a subtype there is a 400), `activitySubType`, `excludedActivitySubTypes`, `eventType`, `startDate`/`endDate`, min/max distance (m), duration and elapsed duration (s), elevation gain (m), `minAvgPower` (+ | `getActivityWorkouts` | `(activityId: number \| string): Promise<ActivityWorkout[]>` — GETs `/activity-service/activity/{id}/workouts`: the workouts done during the activity, `{index, workoutName, timeCreated, sport, manufacturer, multisportSet, steps, workoutPhrase}` with the steps the device ran (`PersonalApi.ActivityWorkoutDTO`, matched key for key, steps included). `[]` for an activity done without one | yes, 2026-10-23, read-only on a real account: an activity with a `workoutId` returned + | `setActivityDistance` | `(activityId: number \| string, meters: number): Promise<unknown>` — Partial PUT of `summaryDTO.distance` in METERS (no conversion; `createManualActivity` is the one that takes km), the edit Garmin Connect offers for treadmill runs. Garmin recomputes `averageSpeed`/`averageMovingSpeed` from it; duration, calories and laps stay as recorded, so the laps no longer sum to the total. `getActivities` lags the change by a few seconds while `getActivity` shows it at once. Throw+ | `getWeightGoals` | `(startdate: string \| Date, enddate: string \| Date): Promise<WeightGoals \| null>` — GETs `/goal-service/goal/user/effective/weightgoal/{start}/{end}`: `{userGoals, userGoalRanges}`, weights in GRAMS (`PersonalApi.UserGoalsRangeDTO`). No range limit found (1000 days answered); a reversed range throws before any request although Garmin itself accepts one | partially: the envelope read back on both accounts (`userGoals: []`, `userGoalRanges: null`); neither has a weight goal+ `ApiOptions`: `{ method?, params?: QueryParams, json?: unknown, headers?: Record<string,string>, timeoutMs?: number }`. There is no public `retry` field on `ApiOptions` — see section 6 for what that means for POST. `QueryParams` is `Record<string, string | number | readonly (string | number)[] | undefined>`: `undefined` is left out and an ARRAY repeats the key once per element (`requiredField=a&requiredField=b`), which is how Garmin's list parameters take several values; a comma-joined string is+ ### Types from Garmin's Personal API spec (`PersonalApi`, src/generated/personal-api.ts)+ Garmin publishes an OpenAPI spec for its API-key "Personal API" on+ `personal.connectapi.garmin.com`. This library does NOT use that API, but the spec's 26 paths are+ the same services we call, and `npm run smoke:personal-api` found the field names match on+ `connectapi.garmin.com` (2026-10-23). `npm run personal-api-types` generates 106 interfaces from+ it, exported from the package root as the type namespace `PersonalApi` (e.g.+ `PersonalApi.SummaryDTO`), plus `PersonalApi.Endpoints`, a path-to-response-type map usable with+ `client.connectapi<PersonalApi.Endpoints["/hrv-service/hrv/{calendarDate}"]>(…)`.+ - **Every field is optional and nullable**, and every interface keeps `[key: string]: unknown`.+   Garmin leaves out fields that do not apply (a run has no swim cadence), and sends fields the+   spec does not list. The spec is what MAY appear, never what will.+ - Corrections the generator applies, each observed on our host: `LocalTime`/`ConnectDate`/+   `ConnectTimestamp` are strings; `fitnessage/v2/{date}` is one object, not an array; enums are+   open unions (`| (string & {})`).+ - **No field descriptions are copied**, on purpose: the developer site's redistribution terms are+   unknown. The spec is fetched at generation time, never committed. Regenerating needs network.+ - Our response types EXTEND these only where our method calls the very path the probe verified:+   `Activity` (`SummarizedActivityDTO`), `ActivitySummary` (`ActivitySummaryDTO`), `HrvData`,+   `HrvDataRange`, `BodyBatteryEntry`, `DailyStepsEntry`, `WeighInRange`, `UserprofileSettings`,+   `NutritionDailyFoodLog` and `DailySummary`. Where ours calls a different path (`getSleepData`,+   `getHeartRates`, `getUserSummary` use `/{displayName}` variants; `getHeartRateZones` and+   `getFitnessAgeData` use the pre-`v2` paths), the types were deliberately left alone.+ `Garmin` has **201 methods**, every one listed in section 3. If a method is not in that table,

    Showing the first 40 of each.

  • server.json 1 field changed
    version
    0.9.4 0.10.0

  • server.json 1 field changed
    version
    0.9.2 0.9.4