Activities
Listing and searching activities, their detail (splits, weather, HR/power zones, exercise sets), manual creation, file import/upload and download, and the activity/gear association.
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);
36 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.
addGearToActivity
garmin.addGearToActivity(gearUUID: string, activityId: number | string): Promise<GearLinkResult | null>
const result = await garmin.addGearToActivity(activityId, activityId);
Returns
GearLinkResult — 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.
on 404 re-raises as GarminConnectionError ("gear not found (likely retired/removed)")
Verification: live-verified
countActivities
garmin.countActivities(): Promise<number>
const result = await garmin.countActivities();
Returns
number
returns the envelope's totalCount, not the whole response; throws GarminError if Garmin returns nothing or a non-numeric totalCount
Verification: live-verified
createManualActivity
garmin.createManualActivity(startDatetime: string, timeZone: string, typeKey: string, distanceKm: number, durationMin: number, activityName: string): Promise<unknown>
const result = await garmin.createManualActivity("startDatetime", "timeZone", "typeKey", 1, 1, "activityName");
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
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)
Verification: live-verified
createManualActivityFromJson
garmin.createManualActivityFromJson(payload: Record<string, unknown>): Promise<unknown>
const result = await garmin.createManualActivityFromJson("payload");
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
sends payload to Garmin verbatim, no shape validation
Verification: live-verified
deleteActivity
garmin.deleteActivity(activityId: number | string): Promise<unknown>
const result = await garmin.deleteActivity(activityId);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
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
Verification: live-verified
downloadActivity
garmin.downloadActivity(activityId: number | string, format?: ActivityDownloadFormat): Promise<Buffer>
const result = await garmin.downloadActivity(activityId);
Returns
A Buffer of file bytes.
ActivityDownloadFormat is "ORIGINAL" | "TCX" | "GPX" | "KML" | "CSV", default "ORIGINAL"
Verification: live-verified
downloadHealthSnapshot
garmin.downloadHealthSnapshot(requestedDate: string | Date): Promise<Buffer>
const result = await garmin.downloadHealthSnapshot("2026-09-24");
Returns
A Buffer of file bytes.
routed through formatDate, routed through client.download
Verification: live-verified
getActivities
garmin.getActivities(start?: number, limit?: number): Promise<Activity[]>
const result = await garmin.getActivities();
Returns
An array of Activity:
| Field | Type | Always present |
|---|---|---|
activityId | number | yes |
activityName | string | no |
startTimeLocal | string | no |
distance | number | no |
duration | number | no |
activityType | Record<string, 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.
defaults start=0, limit=20
Verification: live-verified
getActivitiesByDate
garmin.getActivitiesByDate(startdate: string | Date, enddate?: string | Date, activitytype?: string, sortorder?: string): Promise<Activity[]>
const result = await garmin.getActivitiesByDate("2026-09-24");
Returns
An array of Activity:
| Field | Type | Always present |
|---|---|---|
activityId | number | yes |
activityName | string | no |
startTimeLocal | string | no |
distance | number | no |
duration | number | no |
activityType | Record<string, 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.
paginates internally: fetches fixed pages of 20, incrementing start by 20, until an empty page (normal end) or 2000 pages without one (throws GarminError)
Verification: live-verified
getActivitiesForDate
garmin.getActivitiesForDate(fordate: string | Date): Promise<ActivitiesForDateResponse | null>
const result = await garmin.getActivitiesForDate("2026-09-24");
Returns
ActivitiesForDateResponse — 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.
passes through unchecked; despite the name, the path is /mobile-gateway/heartRate/..., not an activities-service path
Verification: live-verified
getActivity
garmin.getActivity(activityId: number | string): Promise<Activity>
const result = await garmin.getActivity(activityId);
Returns
Activity:
| Field | Type | Always present |
|---|---|---|
activityId | number | yes |
activityName | string | no |
startTimeLocal | string | no |
distance | number | no |
duration | number | no |
activityType | Record<string, 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.
Verification: live-verified
getActivityDetails
garmin.getActivityDetails(activityId: number | string, maxchart?: number, maxpoly?: number): Promise<ActivityDetails | null>
const result = await garmin.getActivityDetails(activityId);
Returns
ActivityDetails — 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 maxchart=2000, maxpoly=4000, sent as maxChartSize/maxPolylineSize; passes through unchecked
Verification: live-verified
getActivityEventTypes
garmin.getActivityEventTypes(): Promise<ActivityEventType[] | null>
const result = await garmin.getActivityEventTypes();
Returns
An array of ActivityEventType:
| Field | Type | Always present |
|---|---|---|
typeId | number | yes |
typeKey | ActivityEventTypeKey | yes |
sortOrder | number | yes |
GETs /activity-service/activity/eventTypes: nine {typeId, typeKey, sortOrder} entries (race, recreation, specialEvent, training, transportation, touring, geocaching, fitness, uncategorized)
Verification: live-verified
getActivityExerciseSets
garmin.getActivityExerciseSets(activityId: number | string): Promise<ActivityExerciseSets | null>
const result = await garmin.getActivityExerciseSets(activityId);
Returns
ActivityExerciseSets:
| Field | Type | Always present |
|---|---|---|
exerciseSets | 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.
passes through unchecked
Verification: live-verified
getActivityGear
garmin.getActivityGear(activityId: number | string): Promise<ActivityGear[] | null>
const result = await garmin.getActivityGear(activityId);
Returns
An array of ActivityGear — 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.
passes through unchecked; returns an ARRAY, verified live
Verification: live-verified
getActivityHrInTimezones
garmin.getActivityHrInTimezones(activityId: number | string): Promise<ActivityHrInTimezones | null>
const result = await garmin.getActivityHrInTimezones(activityId);
Returns
ActivityHrInTimezones — 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.
passes through unchecked
Verification: live-verified
getActivityPowerInTimezones
garmin.getActivityPowerInTimezones(activityId: number | string): Promise<ActivityPowerInTimezones | null>
const result = await garmin.getActivityPowerInTimezones(activityId);
Returns
ActivityPowerInTimezones — 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.
passes through unchecked
Verification: live-verified
getActivitySplits
garmin.getActivitySplits(activityId: number | string): Promise<ActivitySplits | null>
const result = await garmin.getActivitySplits(activityId);
Returns
ActivitySplits — 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.
passes through unchecked
Verification: live-verified
getActivitySplitSummaries
garmin.getActivitySplitSummaries(activityId: number | string): Promise<ActivitySplitSummaries | null>
const result = await garmin.getActivitySplitSummaries(activityId);
Returns
ActivitySplitSummaries — 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.
passes through unchecked
Verification: live-verified
getActivityTypedSplits
garmin.getActivityTypedSplits(activityId: number | string): Promise<ActivityTypedSplits | null>
const result = await garmin.getActivityTypedSplits(activityId);
Returns
ActivityTypedSplits — 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.
passes through unchecked; richer detail than getActivitySplits for some activity types (e.g. Bouldering)
Verification: live-verified
getActivityTypes
garmin.getActivityTypes(): Promise<ActivityTypesResponse | null>
const result = await garmin.getActivityTypes();
Returns
ActivityTypesResponse = ActivityType[]
passes through unchecked; ActivityTypesResponse is ActivityType[] (154 entries observed live: {typeId, typeKey, parentTypeId, isHidden, restricted, trimmable}) — an ARRAY, verified live
Verification: live-verified
getActivityWeather
garmin.getActivityWeather(activityId: number | string): Promise<ActivityWeather | null>
const result = await garmin.getActivityWeather(activityId);
Returns
ActivityWeather — 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.
passes through unchecked
Verification: live-verified
getGearActivities
garmin.getGearActivities(gearUUID: string, limit?: number): Promise<GearActivity[]>
const result = await garmin.getGearActivities(activityId);
Returns
An array of GearActivity — 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.
limit clamped to 1000; returns [] on a 404 instead of throwing
Verification: live-verified
getLastActivity
garmin.getLastActivity(): Promise<Activity | null>
const result = await garmin.getLastActivity();
Returns
Activity:
| Field | Type | Always present |
|---|---|---|
activityId | number | yes |
activityName | string | no |
startTimeLocal | string | no |
distance | number | no |
duration | number | no |
activityType | Record<string, 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.
delegates to getActivities(0, 1), returns the last element or null
Verification: live-verified
getPersonalRecord
garmin.getPersonalRecord(): Promise<PersonalRecords | null>
const result = await garmin.getPersonalRecord();
Returns
PersonalRecords = PersonalRecord[]
GETs /personalrecord-service/personalrecord/prs/{displayName}; no args; passes through unchecked. 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
Verification: live-verified
getProgressSummaryBetweenDates
garmin.getProgressSummaryBetweenDates(startdate: string | Date, enddate: string | Date, metric?: string, groupbyactivities?: boolean): Promise<ProgressSummary | null>
const result = await garmin.getProgressSummaryBetweenDates("2026-09-24", "2026-09-24");
Returns
ProgressSummary — 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 metric="distance", groupbyactivities=true; both dates routed through formatDate; passes through unchecked
Verification: live-verified
importActivity
garmin.importActivity(file: Blob, filename: string): Promise<ImportActivityResult>
const result = await garmin.importActivity(file, "filename");
Returns
ImportActivityResult — 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.
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):...")
Verification: live-verified
removeGearFromActivity
garmin.removeGearFromActivity(gearUUID: string, activityId: number | string): Promise<GearLinkResult | null>
const result = await garmin.removeGearFromActivity(activityId, activityId);
Returns
GearLinkResult — 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.
PUT, not DELETE; same 404-handling pattern as addGearToActivity
Verification: live-verified
setActivityDescription
garmin.setActivityDescription(activityId: number | string, description: string): Promise<unknown>
const result = await garmin.setActivityDescription(activityId, "description");
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
resolves to null on success. Live-verified: description read back via getActivity after the call
Verification: live-verified
setActivityEventType
garmin.setActivityEventType(activityId: number | string, eventType: ActivityEventTypeKey): Promise<unknown>
const result = await garmin.setActivityEventType(activityId, eventType);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
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
Verification: live-verified
setActivityExerciseSets
garmin.setActivityExerciseSets(activityId: number | string, payload: ActivityExerciseSets): Promise<unknown>
const result = await garmin.setActivityExerciseSets(activityId, payload);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
replace-all semantics, payload sent verbatim. See gotchas for the payload shape Garmin actually requires (undocumented by Garmin)
Verification: live-verified
setActivityFeel
garmin.setActivityFeel(activityId: number | string, feel: ActivityFeel | null): Promise<unknown>
const result = await garmin.setActivityFeel(activityId, feel);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
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
Verification: live-verified
setActivityName
garmin.setActivityName(activityId: number | string, activityName: string): Promise<unknown>
const result = await garmin.setActivityName(activityId, "activityName");
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
resolves to null on success. Live-verified: value read back via getActivity after the call, not just that the request was accepted
Verification: live-verified
setActivityPerceivedEffort
garmin.setActivityPerceivedEffort(activityId: number | string, rpe: number | null): Promise<unknown>
const result = await garmin.setActivityPerceivedEffort(activityId, 1);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
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
Verification: live-verified
setActivityType
garmin.setActivityType(activityId: number | string, typeId: number, typeKey: string, parentTypeId: number): Promise<unknown>
const result = await garmin.setActivityType(activityId, activityId, "typeKey", activityId);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
resolves to null on success. Live-verified: activityTypeDTO read back via getActivity after the call
Verification: live-verified
uploadActivity
garmin.uploadActivity(file: Blob, filename: string): Promise<UploadActivityResult | null>
const result = await garmin.uploadActivity(file, "filename");
Returns
UploadActivityResult — 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.
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)
Verification: live-verified