Method reference
193 methods · garminconnect-js 0.9.0
193 of 193 shown
| Method | Description | Tags |
|---|---|---|
| addBodyComposition | builds a `.fit` binary in memory (`src/util/fit.ts`, a hand-rolled FIT encoder) and uploads it via `client.upload` to `/upload-service/upload`; `weight` validated positive/finite, throws `GarminError` otherwise; passes `client.upload`'s result through unchecked | write |
| addGearToActivity | on 404 re-raises as `GarminConnectionError` ("gear not found (likely retired/removed)") | write |
| addHydrationData | raw milliliters, magnitude capped at 10000, negative values allowed; no delete endpoint exists, so **not safely round-trippable** | write |
| addWeighIn | defaults `unitKey="kg"`, `when=new Date()` | write |
| addWeighInWithTimestamps | same raw-value, no-conversion rule as `addWeighIn`; the two timestamps are COUPLED: the local instant resolves from `dateTimestamp` if given (naive strings read as LOCAL time) else from `when` (defaults `new Date()`), and `gmtTimestamp`, when omitted, is derived from THAT resolved instant — never independently from `when`. Both supplied strings are re-formatted rather than forwarded verbatim (a naive `gmtTimestamp` is read as UTC) | write |
| confirmMenstrualPeriodStart | POSTs `/periodichealth-service/menstrualcycle/{periodStartDate}` directly, NOT the `dayview`/`calendar`/`lastconfirmed`/`summary` sub-paths | write |
| countActivities | returns the envelope's `totalCount`, not the whole response; throws `GarminError` if Garmin returns nothing or a non-numeric `totalCount` | read |
| createCalendarEvent | POSTs `/calendar-service/event`. `CalendarEventInput` = `{name, date, startTime?: "HH:MM", timeZoneId?, eventType?, race?, primary?, distance?: {value, unit: meter|kilometer|yard|mile}, goalTimeSeconds?, location?, url?, note?}`; only `name`/`date` are required by Garmin (`'addEvent.arg3.eventName' must not be blank` otherwise). Garmin forces every user-created event **PRIVATE and unshareable** whatever is sent, sets `isTrainingEvent` whenever `isPrimaryEvent` is, and answers an unknown `eventType` with a 500. It also rewrote `Europe/Zagreb` to `Europe/Paris` (same offset) while keeping `Amer… | write |
| createCourse | POSTs `/course-service/course` with the template Garmin's web client sends. `CourseInput` = `{ name, geoPoints, coursePoints?, activityTypeId? = 1 (running), privacy? = "private" }`; privacy maps to `rulePK` public 1 / private 2. Garmin computes `distanceMeter` and elevation. Throws `GarminError` before any request on an empty name or fewer than two geoPoints | write |
| createCourseFromGpx | `importCourseGpx` then `createCourse`. Name defaults to the GPX `<name>`, then the filename without `.gpx` | write |
| createCustomFood | **A PUT**, not a POST, to `/nutrition-service/customFood`; `CustomFoodInput` = `{name, calories, servingUnit? = "G", servingSize? = 100, brand?, carbs?, protein?, fat?, fiber?, sugar?, saturatedFat?, transFat?, sodium?, cholesterol?, potassium?, calcium?, iron?, vitaminD?}` per ONE serving, absolute amounts (not %DV); numbers are sent as strings, as Garmin's own client does. Returns the stored food with the `foodId`/`servingId` `logFood` takes. Needs Connect+ | writeConnect+ |
| createGear | POSTs `/gear-service/gear/v2`; defaults `usageType="DISTANCE", notes=""`; `firstUseDate` routed through `formatDate`; `gearType`/`usageType` upper-cased via `validateSportKey`; **converts** `maxUsageDistanceKm` -> `maxUsageDistanceMeters` (`round(km*1000)`, floor 1) and `maxUsageDurationMin` -> `maxUsageDurationSeconds` (`round(min*60)`, floor 1) — the opposite direction from `addWeighIn`'s "send raw" rule; returns the response unchecked | write |
| createManualActivity | **converts units**: `distanceKm * 1000` → meters, `durationMin * 60` → seconds, before building the request body; `startDatetime` is NOT routed through `formatDate` (it's a full local timestamp, not a bare calendar date) — **must include milliseconds**, e.g. `"2026-09-22T10:00:00.000"`; omitting them produced a live HTTP 500 `ValueInstantiationException` from Garmin during verification. Live-verified: the converted `summaryDTO.distance`/`summaryDTO.duration` were read back via `getActivity` and matched the expected meters/seconds exactly (5.5km/30min → 5500m/1800s) | write |
| createManualActivityFromJson | sends `payload` to Garmin verbatim, no shape validation | write |
| deleteActivity | resolves to `null` on success (204). Live-verified on synthetic fixtures created by the probe itself (never against pre-existing data) — create → delete → poll-confirm gone via `getActivities` | destructive |
| deleteBloodPressure | deletes one reading by its `version`, taken from `getBloodPressure` | destructive |
| deleteCalendarEvent | `DELETE /calendar-service/event/{id}`, resolves `null` (204). IRREVERSIBLE | destructive |
| deleteCourse | `DELETE /course-service/course/{courseId}`, resolves `null` (204). IRREVERSIBLE. May 429 "not yet ready" for a few seconds after creation | destructive |
| deleteCustomFood | `DELETE /nutrition-service/customFood/{foodId}`, resolves `null`. IRREVERSIBLE. Needs Connect+ | destructiveConnect+ |
| deleteFoodLogs | `DELETE /nutrition-service/food/logs/{date}` with `{logIds}` as the body — any number in ONE call, regular and quick-add alike. `logId`s are on the entries of `getNutritionDailyFoodLog`. IRREVERSIBLE. Needs Connect+ | destructiveConnect+ |
| deleteGear | `DELETE /gear-service/gear/v2/{gearUUID}`, resolves `null` on success (204). **The UUID must be HYPHENATED** — `getGear` returns them WITHOUT hyphens and that form 404s, so this method re-inserts them for you when handed the bare 32-char form; only hand-rolled URLs hit the 404. IRREVERSIBLE: removes the gear AND its activity history | destructive |
| deleteHeartRateZones | PUTs the stored profile back with `changeState: "DELETED"`, which removes it so the sport falls back to DEFAULT. Refuses DEFAULT; throws for a sport with no profile instead of doing nothing | destructive |
| deleteTrainingPlan | `DELETE /trainingplan-service/trainingplan/trainingplan/{planId}` (the `trainingplan` segment really is doubled) — the request Garmin Connect's "Quit Plan" sends, observed in the web client on 2026-10-06. Resolves `null` (204); the plan's scheduled workouts leave the calendar, completed activities stay. A second call is a 404 `"Training plan not found with ID: …"`. IRREVERSIBLE | destructive |
| deleteWeighIn | Delete weigh in. | destructive |
| deleteWeighIns | no HTTP path of its own: calls `getDailyWeighIns`, then loops `deleteWeighIn` per entry; returns `null` (deletes nothing) if there are zero entries, or more than one entry and `deleteAll` is not `true`; otherwise deletes every entry that day and returns the count. **IRREVERSIBLE** — deletes ALL weigh-ins recorded on `cdate` when it proceeds | destructive |
| deleteWorkout | deletes the template from the workout library, irreversible | destructive |
| displayName | Display name. | read |
| downloadActivity | `ActivityDownloadFormat` is `"ORIGINAL" | "TCX" | "GPX" | "KML" | "CSV"`, default `"ORIGINAL"` | read |
| downloadCourseGpx | GETs `/course-service/course/gpx/{courseId}` through `client.download` | read |
| downloadHealthSnapshot | routed through `formatDate`, routed through `client.download` | read |
| downloadWorkout | FIT-file bytes | read |
| fullName | Full name. | read |
| getActivities | defaults `start=0, limit=20` | read |
| getActivitiesByDate | paginates internally: fetches fixed pages of 20, incrementing `start` by 20, until an empty page (normal end) or 2000 pages without one (throws `GarminError`) | read |
| getActivitiesForDate | despite the name, the path is `/mobile-gateway/heartRate/...`, not an activities-service path | read |
| getActivity | Get activity. | read |
| getActivityDetails | defaults `maxchart=2000, maxpoly=4000`, sent as `maxChartSize`/`maxPolylineSize` | read |
| getActivityEventTypes | GETs `/activity-service/activity/eventTypes`: nine `{typeId, typeKey, sortOrder}` entries (`race`, `recreation`, `specialEvent`, `training`, `transportation`, `touring`, `geocaching`, `fitness`, `uncategorized`) | read |
| getActivityExerciseSets | Get activity exercise sets. | read |
| getActivityGear | returns an ARRAY, verified live | read |
| getActivityHrInTimezones | Get activity hr in timezones. | read |
| getActivityPowerInTimezones | Get activity power in timezones. | read |
| getActivitySplits | Get activity splits. | read |
| getActivitySplitSummaries | Get activity split summaries. | read |
| getActivityTypedSplits | richer detail than `getActivitySplits` for some activity types (e.g. Bouldering) | read |
| getActivityTypes | `ActivityTypesResponse` is `ActivityType[]` (154 entries observed live: `{typeId, typeKey, parentTypeId, isHidden, restricted, trimmable}`) — **an ARRAY, verified live** | read |
| getActivityWeather | Get activity weather. | read |
| getAdaptiveTrainingPlanById | GETs `/trainingplan-service/trainingplan/fbt-adaptive/{planId}`, a distinct sub-path from `getTrainingPlanById`'s `phased` path | read |
| getAdaptiveWorkout | GETs `/workout-service/fbt-adaptive/{uuid}`: a Garmin Coach workout with segments, steps and `estimatedTrainingEffect`. Coach workouts have no `workoutId`, and `getWorkoutById` cannot fetch them — `/workout-service/workout/{uuid}` is a 404. Take the uuid from `getTrainingPlanWorkouts` or `getScheduledWorkoutSummaries` | read |
| getAdhocChallenges | GETs `/adhocchallenge-service/adHocChallenge/historical`; `start` validated non-negative, `limit` validated positive (throws `GarminError` otherwise); returns an ARRAY, verified live | read |
| getAllDayEvents | Get all day events. | read |
| getAllDayStress | Get all day stress. | read |
| getAvailableBadgeChallenges | GETs `/badgechallenge-service/badgeChallenge/available`; same `start`/`limit` validation; same array-not-dict correction and same live `start=0` -> 400 discovery as `getBadgeChallenges` | read |
| getAvailableBadges | GETs `/badge-service/badge/available?showExclusiveBadge=true`; same `badgeImageUrls` as `getEarnedBadges`; stays nullable | read |
| getBadgeChallenges | GETs `/badgechallenge-service/badgeChallenge/completed`; same `start`/`limit` validation as `getAdhocChallenges`; returns an ARRAY, verified live. **Live discovery**: Garmin's server rejects `start=0` with a 400 (`"start should > 0."`) on this endpoint even though the client-side check allows it (non-negative); the 400 is Garmin's server, not a wrong URL — call with `start>=1` in practice | read |
| getBadgeDetail | GETs `/badge-service/badge/detail/v3/{badgeId}`, the request Garmin Connect's web app makes when a badge is opened; `badgeId` validated as a positive integer. Returns an OBJECT: the `getEarnedBadges` fields plus `relatedBadges` (the rest of the series, each with `earnedByMe`), `badgeAssocType`/`badgeAssocDataId`/`badgeAssocDataName` (for `"activityId"`, the activity that earned it) and `followings`. Works for unearned badges. An unknown id is a **400** `GarminHttpError`, not a 404. The badge and each `relatedBadges` entry gain `badgeImageUrls` (see `getEarnedBadges`). Carries no description t… | read |
| getBloodPressure | `enddate` defaults to `startdate` | read |
| getBodyBattery | Get body battery. | read |
| getBodyBatteryEvents | Get body battery events. | read |
| getBodyComposition | GETs `/weight-service/weight/dateRange`; `enddate` defaults to `startdate`; throws `GarminError` if `startdate > enddate` `getStatsAndBody` (wellness service) now delegates to this instead of inlining its own copy of the same call | read |
| getCalendarEvent | GETs `/calendar-service/event/{id}`; a missing id is a 404 `GarminHttpError` | read |
| getCaloriesDaily | merges active (metricId 22) and resting/BMR (metricId 23) series into `[{calendarDate, active, resting, total}]` | read |
| getCourse | GETs `/course-service/course/{courseId}`; a missing id is a 404 `GarminHttpError` ("Course not found : {id}") | read |
| getCustomFoods | GETs `/nutrition-service/customFood` with `includeContent=true`: `{customFoods, moreDataAvailable}`. **`limit` is capped at 20** (Garmin 400s above it). There is NO get-by-id: `GET /customFood/{id}` is a 405, so search by name to read one back. Needs Connect+ | readConnect+ |
| getCustomFoodServingUnits | GETs `/nutrition-service/metadata/customFoodServingUnits` (13 units). Needs Connect+ | readConnect+ |
| getCyclingFtp | latest value only; use `getFunctionalThresholdPowerRange` for history | read |
| getDailyStats | GETs `/usersummary-service/stats/daily/{start}/{end}?statsType=CALORIES|STEPS`; `statsType` is exactly `"CALORIES"` or `"STEPS"` (any other value, lower-case included, is a Garmin 404). Garmin 400s when `end - start > 27`, so longer ranges are fetched in 28-day windows and the rows concatenated; Garmin's per-request `aggregations` (averages) are DROPPED rather than returned wrong for a chunked range. A range with no data returns `[]` (Garmin answers `null`). Overlaps `getDailySteps`/`getCaloriesDaily`; this one returns total/active/resting calories together, and steps with `totalDistance` and… | read |
| getDailySteps | auto-chunks ranges over Garmin's 28-day-per-request limit into ≤28-day windows and concatenates; a single request within the limit passes its (possibly `null`) result through unchecked | read |
| getDailyWeighIns | GETs `/weight-service/weight/dayview/{cdate}?includeAll=true` | read |
| getDeviceAlarms | no HTTP path of its own: calls `getDevices()` once, then `getDeviceSettings(device.deviceId)` once per device (N+1 fan-out, sequential by design — do not parallelize), concatenating each device's `alarms`; a device with no alarms contributes nothing, never throws for that case | read |
| getDeviceLastUsed | also used internally by `pushWorkoutToDevice` to resolve a missing `deviceId` | read |
| getDevices | undocumented per-device shape, `deviceId` is the field the other device methods key off of | read |
| getDeviceSettings | `deviceId` coerced to an int, validated positive, re-stringified before being placed in the path Two-call sequence: get a `deviceId` from a `getDevices()` entry first, then pass it here | read |
| getDeviceSolarData | the only raising method in this group: throws `GarminConnectionError` if the response is falsy or missing the `deviceSolarInput` key; returns `resp.deviceSolarInput`, NOT the envelope. `enddate` defaults to `startdate`, and `singleDayView` is sent `"true"` exactly when `enddate` was omitted | read |
| getEarnedBadges | GETs `/badge-service/badge/earned`; each badge gains `badgeImageUrls: { small, large }`, added by this library (Garmin sends no image URL): `https://connect.garmin.com/images/badges/xxhdpi/badge_<badgeUuid ?? badgeId>_sml.png` (`_lrg.png` for `large`), the URL Garmin Connect's web app builds; public, no session. Every other field passes through unchanged; stays nullable (does NOT coalesce to `[]`) | read |
| getEnduranceScore | TWO branches by presence of `enddate`: no `enddate` hits the single-day endpoint; with `enddate` hits `.../stats` with hard-coded `aggregation="weekly"` | read |
| getFitnessAgeData | Get fitness age data. | read |
| getFloors | throws `GarminError` if Garmin returns nothing | read |
| getFunctionalThresholdPowerRange | defaults `sport="RUNNING"`, `aggregation="daily"`; `sport` upper-cased and validated (`^[A-Z_]+$`); `aggregation` restricted to `{daily,weekly,monthly,yearly}` | read |
| getGear | hits `/gear-service/gear/filterGear?userProfilePk=...`; returns an ARRAY of gear entries, verified live. This is the dedicated gear-CRUD service (`src/services/gear.ts`), distinct from `getActivityGear` (activities service, reuses the same base URL with `activityId` instead) | read |
| getGearActivities | `limit` clamped to 1000; returns `[]` on a 404 instead of throwing | read |
| getGearDefaults | GETs `/gear-service/gear/user/{userProfileNumber}/activityTypes`; returns an ARRAY of `{uuid, activityTypePk, defaultGear}` entries, verified live | read |
| getGearStats | GETs `/gear-service/gear/stats/{gearUUID}`; `gearUUID` validated via `validateUuid` (hex, hyphens optional); returns `{}` on a 404 instead of throwing; other errors re-raised | read |
| getGoals | defaults `status="active", **start=1**, limit=30` — `start` defaults to 1, NOT 0, because goal-service is 1-INDEXED and `start=0` silently returns `[]`; throws `GarminError` before any request for an invalid `status`. **Paginated, multi-call**: starting at `start`, fetches successive pages of `limit` entries (incrementing `start` by `limit` each call) until a page comes back empty/falsy, same fixed-page-size pattern as `getActivitiesByDate`; throws `GarminError` if `MAX_PAGINATED_REQUESTS` (2000) pages are fetched without ever seeing an empty one. **Sends the load-bearing `Sec-Fetch-Site: sam… | read |
| getGolfClubStats | defaults `limit=1000`; validated positive; GETs `/gcs-golfcommunity/api/v2/club/player`; hyphenated query params `per-page` and `include-stats` (literal `"true"`). **An ARRAY, verified live** — the test account returned a JSON ARRAY of 17 club entries (`{id, clubTypeId, shaftLength, flexTypeId, averageDistance, adviceDistance, retired, deleted, lastModifiedTime}`), not a single object | read |
| getGolfScorecard | GETs `/gcs-golfcommunity/api/v2/scorecard/detail`; hyphenated query params `scorecard-ids` and `include-longest-shot-distance` (sent as the literal string `"true"`) | read |
| getGolfShotData | GETs `/gcs-golfcommunity/api/v2/shot/scorecard/{scorecardId}/hole`; `holeNumbers` accepts commas or hyphens as separators (spaces stripped), re-joined with `-` before sending as the hyphenated `hole-numbers` param; **if any requested hole number is >9, the filter is silently dropped and all 18 holes are requested instead** (Garmin's endpoint drops double-digit hole numbers from a filtered query); omitting `holeNumbers` also fetches all 18 | read |
| getGolfSummary | defaults `start=0, limit=100`; `start` validated non-negative, `limit` validated positive (throws `GarminError` otherwise); query params are literally hyphenated (`per-page`, `start`), matching Garmin's own naming. **An OBJECT, not an array, verified live** — the test account (0 rounds recorded) returned a single pagination-envelope OBJECT `{pageNumber, rowsPerPage, totalRows}`, not an array | read |
| getGolfUserStats | GETs `/gcs-golfcommunity/api/v2/player/stats`; handicap and strokes-gained overview, no params | read |
| getHeartRates | Get heart rates. | read |
| getHeartRateZones | Get heart rate zones. | read |
| getHillScore | TWO branches by presence of `enddate`, same shape as `getEnduranceScore` but the range branch hard-codes `aggregation="daily"` (NOT `"weekly"` — do not conflate the two) | read |
| getHrvData | Get hrv data. | read |
| getHrvDataRange | Get hrv data range. | read |
| getHydrationData | Get hydration data. | read |
| getInProgressBadges | no HTTP path of its own: calls `getEarnedBadges()` and `getAvailableBadges()`, filters each with an in-progress predicate (progress truthy; if `progress === target`, only "in progress" when `badgeLimitCount` is set and `badgeEarnedNumber < badgeLimitCount`), then merges both filtered lists into a `Map` keyed by `badgeId` (available overwrites earned on collision, keeping the earned entry's position); never raises — a `null` from either call is treated as `[]`. Badges carry `badgeImageUrls` (inherited from the two calls) | read |
| getInprogressVirtualChallenges | GETs `/badgechallenge-service/virtualChallenge/inProgress`; **asymmetric validation**: `start` validated POSITIVE here (rejects `start=0`), unlike the non-negative `start` on the four challenge methods above; `limit` validated positive; same array-not-dict correction | read |
| getIntensityMinutesData | Get intensity minutes data. | read |
| getLactateThreshold | defaults `latest=true`, `aggregation="daily"`. TWO DIFFERENT branches: `latest=true` returns `{speed_and_heart_rate, power}` from two GETs; `latest=false` (requires `startDate`, throws otherwise) returns `{speed, heart_rate, power}` from three GETs | read |
| getLastActivity | delegates to `getActivities(0, 1)`, returns the last element or `null` | read |
| getLifestyleLoggingData | GETs `/lifestylelogging-service/dailyLog/{cdate}` Grouped under `misc` per the plan's explicit instruction, even though it superficially resembles a wellness-daily endpoint | read |
| getMaxMetrics | the date is repeated twice in the path (start=end=cdate) | read |
| getMaxMetricsRange | throws `GarminError` if `start > end` | read |
| getMenstrualCalendarData | Garmin rejects windows of 92+ inclusive days; not enforced here (caller's responsibility) | read |
| getMenstrualCycleSummary | Get menstrual cycle summary. | read |
| getMenstrualDataForDate | Get menstrual data for date. | read |
| getMenstrualLastConfirmed | Get menstrual last confirmed. | read |
| getMenstrualReports | defaults `numberOfCycles=6, nextReport=false, reportType="CYCLE"`, `todayCalendarDate` defaults to today; `numberOfCycles` restricted to `{1,6,12}` or throws `GarminError` | read |
| getMorningTrainingReadiness | delegates to `getTrainingReadiness`, no HTTP call of its own; filters for `inputContext === "AFTER_WAKEUP_RESET"`, falls back to the first entry; `null` for a falsy or empty result | read |
| getNextScheduledWorkout | computed from two `getScheduledWorkouts` calls (this month + next, handling Dec->Jan rollover); returns `{}` if nothing matches, never throws | read |
| getNonCompletedBadgeChallenges | GETs `/badgechallenge-service/badgeChallenge/non-completed`; same `start`/`limit` validation; same array-not-dict correction and same live `start=0` -> 400 discovery as `getBadgeChallenges` | read |
| getNutritionDailyFoodLog | GETs `/nutrition-service/food/logs/{cdate}` | read |
| getNutritionDailyMeals | GETs `/nutrition-service/meals/{cdate}` | read |
| getNutritionDailySettings | GETs `/nutrition-service/settings/{cdate}` | read |
| getNutritionFoodLogRange | GETs `/nutrition-service/food/logs/range?startDate&endDate`: `{dailyNutritionSummaries}`, one day-log per day that has anything logged. The ONLY food-logging call that works without Connect+ (returns no days then) | read |
| getPersonalRecord | GETs `/personalrecord-service/personalrecord/prs/{displayName}`; no args `PersonalRecords` is `PersonalRecord[]` — **an ARRAY, verified live** (the test account returned `array[0]`). Note the plural type name: the method name is singular but the payload is a list | read |
| getPowerZones | Get power zones. | read |
| getPowerZonesForSport | `sport` upper-cased and validated the same way as `getFunctionalThresholdPowerRange` | read |
| getPregnancySummary | Get pregnancy summary. | read |
| getPrimaryTrainingDevice | Get primary training device. | read |
| getProgressSummaryBetweenDates | defaults `metric="distance", groupbyactivities=true`; both dates routed through `formatDate` | read |
| getRacePredictions | TWO branches, all-or-nothing params (throws on a partial combination): no params hits `.../latest/{displayName}`; all three hit `.../{type}/{displayName}`, capped at a 366-day span | read |
| getRespirationData | Get respiration data. | read |
| getRhrDaily | reshapes `allMetrics.metricsMap` into `[{calendarDate, value}]`, dropping null values | read |
| getRhrDay | Get rhr day. | read |
| getRunningTolerance | defaults `aggregation="weekly"`; restricted to `{daily,weekly}` (narrower than the FTP/lactate methods) | read |
| getScheduledWorkoutById | uses a DIFFERENT base (`/workout-service/schedule`) than `getScheduledWorkouts` (`/calendar-service`) | read |
| getScheduledWorkouts | `month` is 1-12 on the way in, converted to 0-indexed on the wire; validates `year>=2000`, `month` 1-12 | read |
| getScheduledWorkoutSummaries | POSTs the fixed GraphQL query `workoutScheduleSummariesScalar(startDate, endDate)`: every scheduled workout in the range, plan or self-scheduled (`tpType: null`), as compact rows whose `scheduledWorkoutId` is what `unscheduleWorkout` takes — `null` for a Garmin Coach workout, which also has `tpType: null` like a self-scheduled one (tell them apart by `fbtAdaptivePlanId`). Dates are validated before they are spliced into the query. A GraphQL error arrives as HTTP 200 with `errors`, turned into a `GarminError`. **It LAGS writes**: a just-scheduled workout is in the month feed at once but absent… | read |
| getSharedCalendarEvent | GETs `/calendar-service/event/{uuid}/shareable`: an event from Garmin's public events catalogue (Races & Events -> Find an Event), with `locationStartPoint`, `subscribersCount`, `provider` and `subscribed`. Works WITHOUT subscribing (`subscribed: false`). The plain `/event/{uuid}` is a 404. Events you create never get a shareable uuid (Garmin forces them private) | read |
| getSleepDaily | Garmin's endpoint has a documented **28-day-per-request limit**; ranges beyond that are auto-chunked, de-duplicated by `calendarDate`, and sorted | read |
| getSleepData | Get sleep data. | read |
| getSpo2Data | coerces a string `lastSevenDaysAvgSpO2` to a number | read |
| getStats | alias of `getUserSummary` | read |
| getStatsAndBody | merges `getUserSummary` with the body-composition `totalAverage` block, delegating to `getBodyComposition` (bodyComposition service) for the latter | read |
| getStepsData | Get steps data. | read |
| getStressData | identical URL to `getAllDayStress`; an alias | read |
| getTrainingPlanById | GETs `/trainingplan-service/trainingplan/phased/{planId}` | read |
| getTrainingPlans | GETs `/trainingplan-service/trainingplan/plans`; no params | read |
| getTrainingPlanWorkouts | POSTs GraphQL `trainingPlanScalar(calendarDate, lang, firstDayOfWeek)` — all three arguments are REQUIRED by Garmin — and unwraps `trainingPlanWorkoutScheduleDTOS`: one entry per enrolled plan, `{trainingPlanId, planName, trainingPlanClassification, trainingPlanDetailsDTO, workoutScheduleSummaries}`. Garmin picks the window (18 workouts across several weeks for an ITP plan); `[]` with no plan | read |
| getTrainingReadiness | Get training readiness. | read |
| getTrainingStatus | Get training status. | read |
| getUserProfile | Cached per instance; every date-scoped endpoint needs the display name. | read |
| getUserprofileSettings | GETs `/userprofile-service/userprofile/settings` (SINGULAR "settings", distinct from `getUserSettings`'s "user-settings" — the two paths are one character apart and easy to transpose) Three profile endpoints, three methods: `getUserProfile()` reads `/userprofile-service/socialProfile` (and backs `displayName`/`fullName`/`userName`), `getUserSettings()` reads `/userprofile-service/userprofile/user-settings` (and backs `unitSystem`), and this one reads `.../settings` | read |
| getUserSettings | Cached per instance, same eviction-on-failure pattern as `getUserProfile()`. | read |
| getUserSummary | Get user summary. | read |
| getWeeklyIntensityMinutes | Get weekly intensity minutes. | read |
| getWeeklySteps | `weeks` defaults to 52, must be a positive integer | read |
| getWeeklyStress | same `weeks` default/validation as `getWeeklySteps` | read |
| getWeighIns | Get weigh ins. | read |
| getWorkoutById | Get workout by id. | read |
| getWorkouts | defaults `start=0, limit=100`, stays nullable (does NOT coalesce to `[]`) | read |
| hasConnectPlus | whether the account has a Garmin Connect+ subscription, read from the cached user profile (`/userprofile-service/socialProfile`, so usually no extra request): `true` when `hasPremiumSocialIcon` is `true` or `userRoles` holds any `ROLE_SP_FEATURE_n` entry. The methods in `CONNECT_PLUS_METHODS` (`searchFoods`, `getCustomFoods`, `getCustomFoodServingUnits`, `createCustomFood`, `updateCustomFood`, `deleteCustomFood`, `logFood`, `quickAddFood`, `deleteFoodLogs`) need it; without it they throw `GarminConnectPlusRequiredError` instead of the bare 403 Garmin sends | read |
| importActivity | multipart upload to `/upload-service/upload/{ext}` (extension from `filename`, must be `fit`/`gpx`/`tcx`) with the load-bearing `NK`/`origin`/custom `User-Agent` headers that make Garmin treat it as an import rather than a device sync; a 409 is re-raised as `GarminConnectionError` ("Activity already exists (duplicate):...") | write |
| importCourseGpx | multipart POST (field `file`) to `/course-service/course/import`. **Parses only, saves NOTHING**: the result has `courseId: null` and a RESAMPLED track (13 GPX points came back as 37) with null elevations/timestamps; `courseName` is the GPX `<name>`. Only `.gpx` accepted (only `.gpx` verified); throws `GarminError` on an empty response | write |
| initMenstrualCycleSetup | does NOT also update tracking-preference settings (call `updateMenstrualSettings` separately for that); not intended for an already-configured account | write |
| listCalendarEvents | Events also appear in `getScheduledWorkouts`' month feed as `itemType` `event`; this GETs `/calendar-service/events`, with `startDate`/`endDate` when both dates are given (inclusive) or no params for every event on the account; one date alone throws | read |
| listCourses | GETs `/web-gateway/course/owner/` (trailing slash included); returns an ENVELOPE `{ coursesForUser: CourseSummary[] }`, not a bare array | read |
| logFood | PUTs `/nutrition-service/food/logs` with one `REGULAR_LOG` item and returns the whole day's log. `FoodLogInput` = `{date, foodId, servingId, servings? = 1, time?, meal?, source? = "GARMIN", regionCode?, languageCode?}` — pass the catalogue food's `source`/`regionCode`/`languageCode` for a search result. **Needs a `mealId`**, which only exists after Garmin's nutrition setup in the app (400 `mealId must not be null` otherwise; this method throws a clearer error first). The meal is the named one, else the one whose window holds `time`, else SNACKS; with a `meal` and no `time` it picks a time tha… | writeConnect+ |
| logout | clears the configured `TokenStore` (`host.client.tokenStore.clear()`); makes **no HTTP call** (the token is never revoked server-side). Does NOT clear the in-memory tokens already held by the calling `GarminClient` instance — there is no public API to do that, and this method's host is deliberately scoped to `{ client }` only. **NEVER call this against a `FileTokenStore` pointed at `./tokens`** — that is the test harness's live session | destructive |
| pushWorkoutToDevice | multi-call: resolves a missing `deviceId` via `/device-service/deviceservice/mylastused`'s `userDeviceId`, a missing `workoutId` via `getWorkouts(0,1)`'s first result (throws if none), then reads `getWorkoutById(workoutId).workoutName` for the push message on the final POST | write |
| queryGarminGraphql | POSTs the caller's GraphQL body verbatim to `/graphql-gateway/graphql`. **The leading slash is load-bearing**: `connectapi` composes the request URL by plain string concatenation (`` `https://connectapi.${domain}${path}` ``), not `URL`-relative joining, so omitting the leading slash here would silently glue onto the hostname (`connectapi.garmin.comgraphql-gateway/graphql`) rather than 404 — the usual "a 404 means the URL is wrong" heuristic would not even catch it. The leading slash is therefore hardcoded and deliberate; `tests/services/misc.test.ts` pins the literal composed URL | destructive |
| quickAddFood | PUTs `/nutrition-service/food/logs/quickAdd`: an entry by `name` + `calories`/`carbs`/`protein`/`fat` with no food behind it (`QUICK_ADD`). Same meal rules as `logFood`. Needs Connect+ | writeConnect+ |
| removeGearFromActivity | **PUT**, not DELETE; same 404-handling pattern as `addGearToActivity` | write |
| requestReload | POSTs `/wellness-service/wellness/epoch/request/{cdate}` with no JSON body; asks Garmin to reload/recompute a day's data (Garmin offloads older data, so this forces it back) | write |
| scheduleWorkout | `dateStr` routed through `formatDate` | write |
| searchFoods | GETs `/nutrition-service/food/search?searchExpression&start&limit` (defaults 0/20): `{results: Food[], moreDataAvailable}`; catalogue foods carry `source: "FATSECRET"` and several servings each. **Needs Garmin Connect+**, which needs a paired Garmin device — a bare `403 ForbiddenException` without it | readConnect+ |
| setActivityDescription | resolves to `null` on success. Live-verified: `description` read back via `getActivity` after the call | write |
| setActivityEventType | Partial `PUT /activity-service/activity/{id}` with `{activityId, eventTypeDTO: {typeKey}}` — the `typeKey` alone is enough, Garmin fills in `typeId`/`sortOrder`. Throws `GarminError` before any request for a key outside the nine | write |
| setActivityExerciseSets | **replace-all semantics**, `payload` sent verbatim. | destructive |
| setActivityFeel | Partial PUT of `summaryDTO.directWorkoutFeel`, one of 0/25/50/75/100 ("How did you feel?", very weak to very strong); `null` clears it. Garmin stores any number (30 read back as 30) that its apps cannot display, so the method refuses anything else | write |
| setActivityName | resolves to `null` on success. Live-verified: value read back via `getActivity` after the call, not just that the request was accepted | write |
| setActivityPerceivedEffort | Partial PUT of `summaryDTO.directWorkoutRpe`, which Garmin stores **TIMES TEN**: pass RPE 1-10 (integer), it sends 10-100; `null` clears it. Garmin itself rejects 150 with a 400 `MEASUREMENT_NOT_VALID`. Never read-modify-write the whole `summaryDTO` instead: PUTting its stored start-coordinate pair back is a 400 | write |
| setActivityType | resolves to `null` on success. Live-verified: `activityTypeDTO` read back via `getActivity` after the call | write |
| setBloodPressure | validates systolic 70-260, diastolic 40-150, pulse (if given) 20-250, all integers; `when` defaults to `new Date()` | write |
| setGearActivityDefaults | The working way to set gear defaults (the old dedicated default-gear endpoint is dead; there is no `setGearDefault`). Read-modify-writes the v2 record: GETs `/gear-service/gear/v2/{uuid}`, replaces `associatedActivityTypes` with `[{activityTypeKey, defaultGear: true, preferredGear: false}]` per key, PUTs the whole record back. Keys are **lowercase** (`"running"`), matching `createGear` — NOT `setGearDefault`'s upper-cased form. `[]` clears all defaults. Concurrent callers can clobber each other | destructive |
| setHeartRateZones | READ-MODIFY-WRITE of one profile: GETs `/biometric-service/heartRateZones`, overlays `{sport? = "DEFAULT", trainingMethod?, maxHeartRate?, restingHeartRate?, lactateThresholdHeartRate?, zoneFloors?: [5 bpm]}` with `changeState: "CHANGED"`, PUTs `[profile]` (204), then returns the profile READ BACK. A sport with no profile starts from DEFAULT's. Setting `restingHeartRate` also turns off `restingHrAutoUpdateUsed`. **Garmin does NOT recompute floors** when the method or a heart rate changes — send `zoneFloors` too. Floors must be strictly ascending (Garmin 400s `"Zone Floor values must be ascend… | write |
| unitSystem | `measurementSystem` lives on `/userprofile-service/userprofile/user-settings` (under `userData`), not on the social profile. A convenience accessor, not a data endpoint: returns `undefined` rather than throwing if the payload shape is missing. | read |
| unscheduleWorkout | removes the calendar entry without deleting the workout template; irreversible | destructive |
| updateCalendarEvent | READ-MODIFY-WRITE: GETs the event, overlays `{name?, date?, location?, url?, note?, race?, goalTimeSeconds?}` (`null` clears an optional one), PUTs the whole record back to `/calendar-service/event/{id}`; Garmin answers with no body, so it resolves to `null`. Concurrent callers can clobber each other | write |
| updateCourse | READ-MODIFY-WRITE: GETs the record, overlays the changes, PUTs the whole record back to `/course-service/course/{courseId}` — the same request Garmin's own web editor sends on Save (observed 2026-09-24). Concurrent callers can clobber each other. Throws before any request if neither field is given or the name is blank. A just-created course may 429 "not yet ready" | write |
| updateCustomFood | The same PUT carrying both ids. **FULL REPLACE**: a nutrient or brand left out is removed (verified: the brand was dropped). Needs Connect+ | writeConnect+ |
| updateMenstrualCalendar | **full replace, not a merge**; each `cycleDatesLists` group must be non-empty, consecutive calendar dates, and fall within `[startdate, enddate]`, else throws `GarminError` | destructive |
| updateMenstrualDailyLog | **full-day replace, not a merge**; at least one optional field required or throws `GarminError`; `notes: undefined` preserves the existing note, `notes: ""` clears it (distinct, load-bearing); `discharge` rejects combining `"NO_DISCHARGE"` with any other value | destructive |
| updateMenstrualSettings | `settings` must be non-empty or throws `GarminError`; multi-step: GETs `/userprofile-service/userprofile/user-settings`, overlays `settings` onto the current `userMenstrualCycleSettings`, then PUTs the same endpoint, including `id` if resolvable | write |
| updateWorkout | full-replace PUT; forces `workoutId` into the body to match the path id; unlike `uploadWorkout`, a string must resolve to an object, not an array | write |
| uploadActivity | multipart upload to the PLAIN `/upload-service/upload` path, no extension suffix and none of `importActivity`'s load-bearing import headers (ordinary device-sync-shaped upload, distinct from `importActivity`'s spoofed-client import) | write |
| uploadCyclingWorkout | same pattern, default `sportType` `{sportTypeId:2, sportTypeKey:"cycling", displayOrder:2}` | write |
| uploadRunningWorkout | fills the default `running` `sportType` (`{sportTypeId:1, sportTypeKey:"running", displayOrder:1}`) if the caller didn't supply one, then delegates to `uploadWorkout` | write |
| uploadStrengthWorkout | same pattern, default `sportType` `{sportTypeId:5, sportTypeKey:"strength_training", displayOrder:5}` | write |
| uploadSwimmingWorkout | same pattern, default `sportType` `{sportTypeId:4, sportTypeKey:"swimming", displayOrder:3}` | write |
| uploadWorkout | a string is JSON-parsed (throws `GarminError` on invalid JSON or a non-object/array result) | write |
| userName | User name. | read |