Skip to content
Docs menu

Method reference

193 methods · garminconnect-js 0.9.0

193 of 193 shown

MethodDescriptionTags
addBodyCompositionbuilds 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 uncheckedwrite
addGearToActivityon 404 re-raises as `GarminConnectionError` ("gear not found (likely retired/removed)")write
addHydrationDataraw milliliters, magnitude capped at 10000, negative values allowed; no delete endpoint exists, so **not safely round-trippable**write
addWeighIndefaults `unitKey="kg"`, `when=new Date()`write
addWeighInWithTimestampssame 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
confirmMenstrualPeriodStartPOSTs `/periodichealth-service/menstrualcycle/{periodStartDate}` directly, NOT the `dayview`/`calendar`/`lastconfirmed`/`summary` sub-pathswrite
countActivitiesreturns the envelope's `totalCount`, not the whole response; throws `GarminError` if Garmin returns nothing or a non-numeric `totalCount`read
createCalendarEventPOSTs `/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
createCoursePOSTs `/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 geoPointswrite
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+
createGearPOSTs `/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 uncheckedwrite
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
createManualActivityFromJsonsends `payload` to Garmin verbatim, no shape validationwrite
deleteActivityresolves 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
deleteBloodPressuredeletes one reading by its `version`, taken from `getBloodPressure`destructive
deleteCalendarEvent`DELETE /calendar-service/event/{id}`, resolves `null` (204). IRREVERSIBLEdestructive
deleteCourse`DELETE /course-service/course/{courseId}`, resolves `null` (204). IRREVERSIBLE. May 429 "not yet ready" for a few seconds after creationdestructive
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 historydestructive
deleteHeartRateZonesPUTs 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 nothingdestructive
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: …"`. IRREVERSIBLEdestructive
deleteWeighInDelete weigh in.destructive
deleteWeighInsno 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 proceedsdestructive
deleteWorkoutdeletes the template from the workout library, irreversibledestructive
displayNameDisplay name.read
downloadActivity`ActivityDownloadFormat` is `"ORIGINAL" | "TCX" | "GPX" | "KML" | "CSV"`, default `"ORIGINAL"`read
downloadCourseGpxGETs `/course-service/course/gpx/{courseId}` through `client.download`read
downloadHealthSnapshotrouted through `formatDate`, routed through `client.download`read
downloadWorkoutFIT-file bytesread
fullNameFull name.read
getActivitiesdefaults `start=0, limit=20`read
getActivitiesByDatepaginates internally: fetches fixed pages of 20, incrementing `start` by 20, until an empty page (normal end) or 2000 pages without one (throws `GarminError`)read
getActivitiesForDatedespite the name, the path is `/mobile-gateway/heartRate/...`, not an activities-service pathread
getActivityGet activity.read
getActivityDetailsdefaults `maxchart=2000, maxpoly=4000`, sent as `maxChartSize`/`maxPolylineSize`read
getActivityEventTypesGETs `/activity-service/activity/eventTypes`: nine `{typeId, typeKey, sortOrder}` entries (`race`, `recreation`, `specialEvent`, `training`, `transportation`, `touring`, `geocaching`, `fitness`, `uncategorized`)read
getActivityExerciseSetsGet activity exercise sets.read
getActivityGearreturns an ARRAY, verified liveread
getActivityHrInTimezonesGet activity hr in timezones.read
getActivityPowerInTimezonesGet activity power in timezones.read
getActivitySplitsGet activity splits.read
getActivitySplitSummariesGet activity split summaries.read
getActivityTypedSplitsricher 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
getActivityWeatherGet activity weather.read
getAdaptiveTrainingPlanByIdGETs `/trainingplan-service/trainingplan/fbt-adaptive/{planId}`, a distinct sub-path from `getTrainingPlanById`'s `phased` pathread
getAdaptiveWorkoutGETs `/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
getAdhocChallengesGETs `/adhocchallenge-service/adHocChallenge/historical`; `start` validated non-negative, `limit` validated positive (throws `GarminError` otherwise); returns an ARRAY, verified liveread
getAllDayEventsGet all day events.read
getAllDayStressGet all day stress.read
getAvailableBadgeChallengesGETs `/badgechallenge-service/badgeChallenge/available`; same `start`/`limit` validation; same array-not-dict correction and same live `start=0` -> 400 discovery as `getBadgeChallenges`read
getAvailableBadgesGETs `/badge-service/badge/available?showExclusiveBadge=true`; same `badgeImageUrls` as `getEarnedBadges`; stays nullableread
getBadgeChallengesGETs `/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 practiceread
getBadgeDetailGETs `/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
getBodyBatteryGet body battery.read
getBodyBatteryEventsGet body battery events.read
getBodyCompositionGETs `/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 callread
getCalendarEventGETs `/calendar-service/event/{id}`; a missing id is a 404 `GarminHttpError`read
getCaloriesDailymerges active (metricId 22) and resting/BMR (metricId 23) series into `[{calendarDate, active, resting, total}]`read
getCourseGETs `/course-service/course/{courseId}`; a missing id is a 404 `GarminHttpError` ("Course not found : {id}")read
getCustomFoodsGETs `/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+
getCustomFoodServingUnitsGETs `/nutrition-service/metadata/customFoodServingUnits` (13 units). Needs Connect+readConnect+
getCyclingFtplatest value only; use `getFunctionalThresholdPowerRange` for historyread
getDailyStatsGETs `/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
getDailyStepsauto-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 uncheckedread
getDailyWeighInsGETs `/weight-service/weight/dayview/{cdate}?includeAll=true`read
getDeviceAlarmsno 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 caseread
getDeviceLastUsedalso used internally by `pushWorkoutToDevice` to resolve a missing `deviceId`read
getDevicesundocumented per-device shape, `deviceId` is the field the other device methods key off ofread
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 hereread
getDeviceSolarDatathe 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 omittedread
getEarnedBadgesGETs `/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
getEnduranceScoreTWO branches by presence of `enddate`: no `enddate` hits the single-day endpoint; with `enddate` hits `.../stats` with hard-coded `aggregation="weekly"`read
getFitnessAgeDataGet fitness age data.read
getFloorsthrows `GarminError` if Garmin returns nothingread
getFunctionalThresholdPowerRangedefaults `sport="RUNNING"`, `aggregation="daily"`; `sport` upper-cased and validated (`^[A-Z_]+$`); `aggregation` restricted to `{daily,weekly,monthly,yearly}`read
getGearhits `/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 throwingread
getGearDefaultsGETs `/gear-service/gear/user/{userProfileNumber}/activityTypes`; returns an ARRAY of `{uuid, activityTypePk, defaultGear}` entries, verified liveread
getGearStatsGETs `/gear-service/gear/stats/{gearUUID}`; `gearUUID` validated via `validateUuid` (hex, hyphens optional); returns `{}` on a 404 instead of throwing; other errors re-raisedread
getGoalsdefaults `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
getGolfClubStatsdefaults `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 objectread
getGolfScorecardGETs `/gcs-golfcommunity/api/v2/scorecard/detail`; hyphenated query params `scorecard-ids` and `include-longest-shot-distance` (sent as the literal string `"true"`)read
getGolfShotDataGETs `/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 18read
getGolfSummarydefaults `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 arrayread
getGolfUserStatsGETs `/gcs-golfcommunity/api/v2/player/stats`; handicap and strokes-gained overview, no paramsread
getHeartRatesGet heart rates.read
getHeartRateZonesGet heart rate zones.read
getHillScoreTWO 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
getHrvDataGet hrv data.read
getHrvDataRangeGet hrv data range.read
getHydrationDataGet hydration data.read
getInProgressBadgesno 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
getInprogressVirtualChallengesGETs `/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 correctionread
getIntensityMinutesDataGet intensity minutes data.read
getLactateThresholddefaults `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 GETsread
getLastActivitydelegates to `getActivities(0, 1)`, returns the last element or `null`read
getLifestyleLoggingDataGETs `/lifestylelogging-service/dailyLog/{cdate}` Grouped under `misc` per the plan's explicit instruction, even though it superficially resembles a wellness-daily endpointread
getMaxMetricsthe date is repeated twice in the path (start=end=cdate)read
getMaxMetricsRangethrows `GarminError` if `start > end`read
getMenstrualCalendarDataGarmin rejects windows of 92+ inclusive days; not enforced here (caller's responsibility)read
getMenstrualCycleSummaryGet menstrual cycle summary.read
getMenstrualDataForDateGet menstrual data for date.read
getMenstrualLastConfirmedGet menstrual last confirmed.read
getMenstrualReportsdefaults `numberOfCycles=6, nextReport=false, reportType="CYCLE"`, `todayCalendarDate` defaults to today; `numberOfCycles` restricted to `{1,6,12}` or throws `GarminError`read
getMorningTrainingReadinessdelegates 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 resultread
getNextScheduledWorkoutcomputed from two `getScheduledWorkouts` calls (this month + next, handling Dec->Jan rollover); returns `{}` if nothing matches, never throwsread
getNonCompletedBadgeChallengesGETs `/badgechallenge-service/badgeChallenge/non-completed`; same `start`/`limit` validation; same array-not-dict correction and same live `start=0` -> 400 discovery as `getBadgeChallenges`read
getNutritionDailyFoodLogGETs `/nutrition-service/food/logs/{cdate}`read
getNutritionDailyMealsGETs `/nutrition-service/meals/{cdate}`read
getNutritionDailySettingsGETs `/nutrition-service/settings/{cdate}`read
getNutritionFoodLogRangeGETs `/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
getPersonalRecordGETs `/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 listread
getPowerZonesGet power zones.read
getPowerZonesForSport`sport` upper-cased and validated the same way as `getFunctionalThresholdPowerRange`read
getPregnancySummaryGet pregnancy summary.read
getPrimaryTrainingDeviceGet primary training device.read
getProgressSummaryBetweenDatesdefaults `metric="distance", groupbyactivities=true`; both dates routed through `formatDate`read
getRacePredictionsTWO 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 spanread
getRespirationDataGet respiration data.read
getRhrDailyreshapes `allMetrics.metricsMap` into `[{calendarDate, value}]`, dropping null valuesread
getRhrDayGet rhr day.read
getRunningTolerancedefaults `aggregation="weekly"`; restricted to `{daily,weekly}` (narrower than the FTP/lactate methods)read
getScheduledWorkoutByIduses 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-12read
getScheduledWorkoutSummariesPOSTs 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
getSharedCalendarEventGETs `/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
getSleepDailyGarmin's endpoint has a documented **28-day-per-request limit**; ranges beyond that are auto-chunked, de-duplicated by `calendarDate`, and sortedread
getSleepDataGet sleep data.read
getSpo2Datacoerces a string `lastSevenDaysAvgSpO2` to a numberread
getStatsalias of `getUserSummary`read
getStatsAndBodymerges `getUserSummary` with the body-composition `totalAverage` block, delegating to `getBodyComposition` (bodyComposition service) for the latterread
getStepsDataGet steps data.read
getStressDataidentical URL to `getAllDayStress`; an aliasread
getTrainingPlanByIdGETs `/trainingplan-service/trainingplan/phased/{planId}`read
getTrainingPlansGETs `/trainingplan-service/trainingplan/plans`; no paramsread
getTrainingPlanWorkoutsPOSTs 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 planread
getTrainingReadinessGet training readiness.read
getTrainingStatusGet training status.read
getUserProfileCached per instance; every date-scoped endpoint needs the display name.read
getUserprofileSettingsGETs `/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
getUserSettingsCached per instance, same eviction-on-failure pattern as `getUserProfile()`.read
getUserSummaryGet user summary.read
getWeeklyIntensityMinutesGet weekly intensity minutes.read
getWeeklySteps`weeks` defaults to 52, must be a positive integerread
getWeeklyStresssame `weeks` default/validation as `getWeeklySteps`read
getWeighInsGet weigh ins.read
getWorkoutByIdGet workout by id.read
getWorkoutsdefaults `start=0, limit=100`, stays nullable (does NOT coalesce to `[]`)read
hasConnectPluswhether 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 sendsread
importActivitymultipart 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
importCourseGpxmultipart 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 responsewrite
initMenstrualCycleSetupdoes NOT also update tracking-preference settings (call `updateMenstrualSettings` separately for that); not intended for an already-configured accountwrite
listCalendarEventsEvents 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 throwsread
listCoursesGETs `/web-gateway/course/owner/` (trailing slash included); returns an ENVELOPE `{ coursesForUser: CourseSummary[] }`, not a bare arrayread
logFoodPUTs `/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+
logoutclears 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 sessiondestructive
pushWorkoutToDevicemulti-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 POSTwrite
queryGarminGraphqlPOSTs 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 URLdestructive
quickAddFoodPUTs `/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
requestReloadPOSTs `/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
searchFoodsGETs `/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 itreadConnect+
setActivityDescriptionresolves to `null` on success. Live-verified: `description` read back via `getActivity` after the callwrite
setActivityEventTypePartial `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 ninewrite
setActivityExerciseSets**replace-all semantics**, `payload` sent verbatim.destructive
setActivityFeelPartial 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 elsewrite
setActivityNameresolves to `null` on success. Live-verified: value read back via `getActivity` after the call, not just that the request was acceptedwrite
setActivityPerceivedEffortPartial 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 400write
setActivityTyperesolves to `null` on success. Live-verified: `activityTypeDTO` read back via `getActivity` after the callwrite
setBloodPressurevalidates systolic 70-260, diastolic 40-150, pulse (if given) 20-250, all integers; `when` defaults to `new Date()`write
setGearActivityDefaultsThe 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 otherdestructive
setHeartRateZonesREAD-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
unscheduleWorkoutremoves the calendar entry without deleting the workout template; irreversibledestructive
updateCalendarEventREAD-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 otherwrite
updateCourseREAD-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
updateCustomFoodThe 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 valuedestructive
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 resolvablewrite
updateWorkoutfull-replace PUT; forces `workoutId` into the body to match the path id; unlike `uploadWorkout`, a string must resolve to an object, not an arraywrite
uploadActivitymultipart 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
uploadCyclingWorkoutsame pattern, default `sportType` `{sportTypeId:2, sportTypeKey:"cycling", displayOrder:2}`write
uploadRunningWorkoutfills the default `running` `sportType` (`{sportTypeId:1, sportTypeKey:"running", displayOrder:1}`) if the caller didn't supply one, then delegates to `uploadWorkout`write
uploadStrengthWorkoutsame pattern, default `sportType` `{sportTypeId:5, sportTypeKey:"strength_training", displayOrder:5}`write
uploadSwimmingWorkoutsame pattern, default `sportType` `{sportTypeId:4, sportTypeKey:"swimming", displayOrder:3}`write
uploadWorkouta string is JSON-parsed (throws `GarminError` on invalid JSON or a non-object/array result)write
userNameUser name.read