Profile, goals, nutrition, plans & misc
User profile and settings, goals, nutrition logs, training plans, and the odds and ends — lifestyle logging, a data-reload request, the GraphQL passthrough, and logout.
Every method below hangs off a Garmin instance. See Installation & setup for how to construct one:
import { GarminClient, Garmin, FileTokenStore } from "garminconnect-js";
const client = new GarminClient({ tokenStore: new FileTokenStore("./tokens") });
if (!(await client.loadTokens())) throw new Error("not connected to Garmin");
const garmin = new Garmin(client);
30 methods. The verification column says what has been
confirmed against a live Garmin account, not merely unit-tested —
AGENTS.md carries the full evidence per method.
| Method | Verified |
|---|---|
createCustomFood | live-verified · Connect+ |
deleteCustomFood | live-verified · Connect+ |
deleteFoodLogs | live-verified · Connect+ |
deleteTrainingPlan | live-verified |
displayName | live-verified (indirectly) |
fullName | live-verified (indirectly) |
getAdaptiveTrainingPlanById | live-verified |
getCustomFoods | live-verified · Connect+ |
getCustomFoodServingUnits | live-verified · Connect+ |
getGoals | live-verified |
getLifestyleLoggingData | live-verified |
getNutritionDailyFoodLog | live-verified |
getNutritionDailyMeals | live-verified |
getNutritionDailySettings | live-verified |
getNutritionFoodLogRange | live-verified |
getTrainingPlanById | live-verified |
getTrainingPlans | live-verified |
getUserProfile | live-verified |
getUserprofileSettings | live-verified |
getUserSettings | live-verified |
hasConnectPlus | live-verified |
logFood | live-verified · Connect+ |
logout | — not applicable |
queryGarminGraphql | live-verified |
quickAddFood | live-verified · Connect+ |
requestReload | live-verified |
searchFoods | live-verified · Connect+ |
unitSystem | live-verified |
updateCustomFood | live-verified · Connect+ |
userName | live-verified (indirectly) |
createCustomFood
Requires Garmin Connect+. Without it this throws
GarminConnectPlusRequiredError; check first withgarmin.hasConnectPlus().
garmin.createCustomFood(input: CustomFoodInput): Promise<Food | null>
const result = await garmin.createCustomFood(input);
Returns
Food:
| Field | Type | Always present |
|---|---|---|
foodMetaData | { | yes |
foodId | string | yes |
foodName | string | yes |
brandName | string | no |
source | string | yes |
regionCode | string | yes |
languageCode | string | yes |
nutritionContents | FoodServing[] | yes |
foodImages | unknown[] | no |
isFavorite | boolean | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
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+
Verification: live-verified
deleteCustomFood
Requires Garmin Connect+. Without it this throws
GarminConnectPlusRequiredError; check first withgarmin.hasConnectPlus().
garmin.deleteCustomFood(foodId: string): Promise<unknown>
const result = await garmin.deleteCustomFood(activityId);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
DELETE /nutrition-service/customFood/{foodId}, resolves null. IRREVERSIBLE. Needs Connect+
Verification: live-verified
deleteFoodLogs
Requires Garmin Connect+. Without it this throws
GarminConnectPlusRequiredError; check first withgarmin.hasConnectPlus().
garmin.deleteFoodLogs(date: string | Date, logIds: string[]): Promise<unknown>
const result = await garmin.deleteFoodLogs("2026-09-24", ["running"]);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
DELETE /nutrition-service/food/logs/{date} with {logIds} as the body — any number in ONE call, regular and quick-add alike. logIds are on the entries of getNutritionDailyFoodLog. IRREVERSIBLE. Needs Connect+
Verification: live-verified
deleteTrainingPlan
garmin.deleteTrainingPlan(planId: number | string): Promise<unknown>
const result = await garmin.deleteTrainingPlan(activityId);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
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
Verification: live-verified
displayName
garmin.displayName(): Promise<string>
const result = await garmin.displayName();
Returns
string
Verification: live-verified (indirectly)
fullName
garmin.fullName(): Promise<string>
const result = await garmin.fullName();
Returns
string
Verification: live-verified (indirectly)
getAdaptiveTrainingPlanById
garmin.getAdaptiveTrainingPlanById(planId: number | string): Promise<AdaptiveTrainingPlanDetail | null>
const result = await garmin.getAdaptiveTrainingPlanById(activityId);
Returns
AdaptiveTrainingPlanDetail — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
GETs /trainingplan-service/trainingplan/fbt-adaptive/{planId}, a distinct sub-path from getTrainingPlanById's phased path; passes through unchecked
Verification: live-verified
getCustomFoods
Requires Garmin Connect+. Without it this throws
GarminConnectPlusRequiredError; check first withgarmin.hasConnectPlus().
garmin.getCustomFoods(search?: string, start?: number, limit?: number): Promise<CustomFoodList | null>
const result = await garmin.getCustomFoods();
Returns
CustomFoodList:
| Field | Type | Always present |
|---|---|---|
customFoods | Food[] | yes |
moreDataAvailable | boolean | yes |
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+
Verification: live-verified
getCustomFoodServingUnits
Requires Garmin Connect+. Without it this throws
GarminConnectPlusRequiredError; check first withgarmin.hasConnectPlus().
garmin.getCustomFoodServingUnits(): Promise<{servingUnits: {name}[]} | null>
const result = await garmin.getCustomFoodServingUnits();
Returns
{servingUnits: {name}[]}
GETs /nutrition-service/metadata/customFoodServingUnits (13 units). Needs Connect+
Verification: live-verified
getGoals
garmin.getGoals(status?: "active" | "future" | "past", start?: number, limit?: number): Promise<Goal[]>
const result = await garmin.getGoals();
Returns
An array of Goal — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
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: same-origin header on every request — without it goal-service silently returns [] for newer custom accumulation-goal types; no error, no 404, just wrong data
Verification: live-verified
getLifestyleLoggingData
garmin.getLifestyleLoggingData(cdate: string | Date): Promise<LifestyleLoggingData | null>
const result = await garmin.getLifestyleLoggingData("2026-09-24");
Returns
LifestyleLoggingData — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
GETs /lifestylelogging-service/dailyLog/{cdate}; passes through unchecked. Grouped under misc per the plan's explicit instruction, even though it superficially resembles a wellness-daily endpoint
Verification: live-verified
getNutritionDailyFoodLog
garmin.getNutritionDailyFoodLog(cdate: string | Date): Promise<NutritionDailyFoodLog | null>
const result = await garmin.getNutritionDailyFoodLog("2026-09-24");
Returns
NutritionDailyFoodLog — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
GETs /nutrition-service/food/logs/{cdate}; passes through unchecked
Verification: live-verified
getNutritionDailyMeals
garmin.getNutritionDailyMeals(cdate: string | Date): Promise<NutritionDailyMeals | null>
const result = await garmin.getNutritionDailyMeals("2026-09-24");
Returns
NutritionDailyMeals — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
GETs /nutrition-service/meals/{cdate}; passes through unchecked
Verification: live-verified
getNutritionDailySettings
garmin.getNutritionDailySettings(cdate: string | Date): Promise<NutritionDailySettings | null>
const result = await garmin.getNutritionDailySettings("2026-09-24");
Returns
NutritionDailySettings — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
GETs /nutrition-service/settings/{cdate}; passes through unchecked
Verification: live-verified
getNutritionFoodLogRange
garmin.getNutritionFoodLogRange(startdate: string | Date, enddate: string | Date): Promise<NutritionFoodLogRange | null>
const result = await garmin.getNutritionFoodLogRange("2026-09-24", "2026-09-24");
Returns
NutritionFoodLogRange:
| Field | Type | Always present |
|---|---|---|
dailyNutritionSummaries | NutritionDailyFoodLog[] | yes |
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)
Verification: live-verified
getTrainingPlanById
garmin.getTrainingPlanById(planId: number | string): Promise<TrainingPlanDetail | null>
const result = await garmin.getTrainingPlanById(activityId);
Returns
TrainingPlanDetail — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
GETs /trainingplan-service/trainingplan/phased/{planId}; passes through unchecked
Verification: live-verified
getTrainingPlans
garmin.getTrainingPlans(): Promise<TrainingPlansResult | null>
const result = await garmin.getTrainingPlans();
Returns
TrainingPlansResult:
| Field | Type | Always present |
|---|---|---|
trainingPlanList | unknown[] | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
GETs /trainingplan-service/trainingplan/plans; no params; passes through unchecked
Verification: live-verified
getUserProfile
garmin.getUserProfile(): Promise<SocialProfile>
const result = await garmin.getUserProfile();
Returns
SocialProfile:
| Field | Type | Always present |
|---|---|---|
displayName | string | yes |
userName | string | yes |
fullName | string | yes |
profileId | number | yes |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
Verification: live-verified
getUserprofileSettings
garmin.getUserprofileSettings(): Promise<UserprofileSettings | null>
const result = await garmin.getUserprofileSettings();
Returns
UserprofileSettings — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
GETs /userprofile-service/userprofile/settings (SINGULAR "settings", distinct from getUserSettings's "user-settings" — the two paths are one character apart and easy to transpose); passes through unchecked. 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
Verification: live-verified
getUserSettings
garmin.getUserSettings(): Promise<UserSettings>
const result = await garmin.getUserSettings();
Returns
UserSettings:
| Field | Type | Always present |
|---|---|---|
id | number | no |
userData | { | no |
measurementSystem | string | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
Verification: live-verified
hasConnectPlus
garmin.hasConnectPlus(): Promise<boolean>
const result = await garmin.hasConnectPlus();
Returns
boolean
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
Verification: live-verified
logFood
Requires Garmin Connect+. Without it this throws
GarminConnectPlusRequiredError; check first withgarmin.hasConnectPlus().
garmin.logFood(input: FoodLogInput): Promise<NutritionDailyFoodLog | null>
const result = await garmin.logFood(input);
Returns
NutritionDailyFoodLog — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
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 that fits. Garmin VALIDATES the pairing: a snack inside LUNCH's window is a 400 "Meal time for Snacks overlap with meal type: LUNCH". Needs Connect+
Verification: live-verified
logout
garmin.logout(): Promise<void>
const result = await garmin.logout();
Returns
Nothing.
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
Verification: — not applicable
queryGarminGraphql
garmin.queryGarminGraphql(query: Record<string, unknown>): Promise<GraphqlResult | null>
const result = await garmin.queryGarminGraphql("query");
Returns
GraphqlResult — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
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
Verification: live-verified
quickAddFood
Requires Garmin Connect+. Without it this throws
GarminConnectPlusRequiredError; check first withgarmin.hasConnectPlus().
garmin.quickAddFood(input: QuickAddInput): Promise<NutritionDailyFoodLog | null>
const result = await garmin.quickAddFood(input);
Returns
NutritionDailyFoodLog — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
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+
Verification: live-verified
requestReload
garmin.requestReload(cdate: string | Date): Promise<ReloadRequestResult | null>
const result = await garmin.requestReload("2026-09-24");
Returns
ReloadRequestResult — an object whose fields this library does not model. Garmin's response is passed through unparsed, so read one to see what you get, or use a Record<string, unknown> and narrow it yourself.
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)
Verification: live-verified
searchFoods
Requires Garmin Connect+. Without it this throws
GarminConnectPlusRequiredError; check first withgarmin.hasConnectPlus().
garmin.searchFoods(query: string, start?: number, limit?: number): Promise<FoodSearchResult | null>
const result = await garmin.searchFoods("query");
Returns
FoodSearchResult:
| Field | Type | Always present |
|---|---|---|
results | Food[] | yes |
moreDataAvailable | boolean | yes |
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
Verification: live-verified
unitSystem
garmin.unitSystem(): Promise<string | undefined>
const result = await garmin.unitSystem();
Returns
string | undefined
Verification: live-verified
updateCustomFood
Requires Garmin Connect+. Without it this throws
GarminConnectPlusRequiredError; check first withgarmin.hasConnectPlus().
garmin.updateCustomFood(foodId: string, servingId: string, input: CustomFoodInput): Promise<Food | null>
const result = await garmin.updateCustomFood(activityId, activityId, input);
Returns
Food:
| Field | Type | Always present |
|---|---|---|
foodMetaData | { | yes |
foodId | string | yes |
foodName | string | yes |
brandName | string | no |
source | string | yes |
regionCode | string | yes |
languageCode | string | yes |
nutritionContents | FoodServing[] | yes |
foodImages | unknown[] | no |
isFavorite | boolean | no |
Plus every other field Garmin sends: this type carries an index signature because the real response is wider than the fields above, which are the ones this library relies on or has observed. Read an actual response before depending on a field that is not listed.
The same PUT carrying both ids. FULL REPLACE: a nutrient or brand left out is removed (verified: the brand was dropped). Needs Connect+
Verification: live-verified
userName
garmin.userName(): Promise<string>
const result = await garmin.userName();
Returns
string
Verification: live-verified (indirectly)