Skip to content
Docs menu

Training metrics

Training status and readiness, race predictions, FTP, lactate threshold, heart-rate and power zones, endurance and hill scores, fitness age.

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);

18 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.

MethodVerified
deleteHeartRateZoneslive-verified
getCyclingFtplive-verified
getEnduranceScorelive-verified
getFitnessAgeDatalive-verified
getFunctionalThresholdPowerRangelive-verified
getHeartRateZoneslive-verified
getHillScorelive-verified
getLactateThresholdlive-verified
getMaxMetricslive-verified
getMaxMetricsRangelive-verified
getMorningTrainingReadinesslive-verified
getPowerZoneslive-verified
getPowerZonesForSportlive-verified
getRacePredictionslive-verified
getRunningTolerancelive-verified
getTrainingReadinesslive-verified
getTrainingStatuslive-verified
setHeartRateZoneslive-verified

deleteHeartRateZones

garmin.deleteHeartRateZones(sport: string): Promise<unknown>
const result = await garmin.deleteHeartRateZones("sport");

Returns

unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.

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

Verification: live-verified

getCyclingFtp

garmin.getCyclingFtp(): Promise<CyclingFtpResult | null>
const result = await garmin.getCyclingFtp();

Returns

CyclingFtpResult = Record<string, unknown> | Record<string, unknown>[]

latest value only; use getFunctionalThresholdPowerRange for history

Verification: live-verified

getEnduranceScore

garmin.getEnduranceScore(startdate: string | Date, enddate?: string | Date): Promise<EnduranceScoreResult | null>
const result = await garmin.getEnduranceScore("2026-09-24");

Returns

EnduranceScoreResult — 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.

TWO branches by presence of enddate, see gotchas: no enddate hits the single-day endpoint; with enddate hits .../stats with hard-coded aggregation="weekly"

Verification: live-verified

getFitnessAgeData

garmin.getFitnessAgeData(cdate: string | Date): Promise<FitnessAgeResult | null>
const result = await garmin.getFitnessAgeData("2026-09-24");

Returns

FitnessAgeResult — 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

getFunctionalThresholdPowerRange

garmin.getFunctionalThresholdPowerRange(start: string | Date, end: string | Date, sport?: string, aggregation?: FtpAggregation): Promise<FtpRangeResult | null>
const result = await garmin.getFunctionalThresholdPowerRange("2026-09-24", "2026-09-24");

Returns

FtpRangeResult = Record<string, unknown> | Record<string, unknown>[]

defaults sport="RUNNING", aggregation="daily"; sport upper-cased and validated (^[A-Z_]+$); aggregation restricted to {daily,weekly,monthly,yearly}; passes through unchecked

Verification: live-verified

getHeartRateZones

garmin.getHeartRateZones(): Promise<HeartRateZoneEntry[] | null>
const result = await garmin.getHeartRateZones();

Returns

An array of HeartRateZoneEntry:

FieldTypeAlways present
sportstringno
trainingMethodHeartRateZoneMethodno
zone1Floornumberno
zone2Floornumberno
zone3Floornumberno
zone4Floornumberno
zone5Floornumberno
maxHeartRateUsed`numbernull`
restingHeartRateUsed`numbernull`
lactateThresholdHeartRateUsed`numbernull`
restingHrAutoUpdateUsedbooleanno
changeStatestringno

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

getHillScore

garmin.getHillScore(startdate: string | Date, enddate?: string | Date): Promise<HillScoreResult | null>
const result = await garmin.getHillScore("2026-09-24");

Returns

HillScoreResult — 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.

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), see gotchas

Verification: live-verified

getLactateThreshold

garmin.getLactateThreshold(latest?: boolean, startDate?: string | Date, endDate?: string | Date, aggregation?: FtpAggregation): Promise<LactateThresholdLatest | LactateThresholdRange>
const result = await garmin.getLactateThreshold();

Returns

LactateThresholdLatest | LactateThresholdRange

defaults latest=true, aggregation="daily". TWO DIFFERENT branches, see gotchas: 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

Verification: live-verified

getMaxMetrics

garmin.getMaxMetrics(cdate: string | Date): Promise<MaxMetricsResult | null>
const result = await garmin.getMaxMetrics("2026-09-24");

Returns

MaxMetricsResult — 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.

the date is repeated twice in the path (start=end=cdate); passes through unchecked

Verification: live-verified

getMaxMetricsRange

garmin.getMaxMetricsRange(start: string | Date, end: string | Date): Promise<MaxMetricsResult | null>
const result = await garmin.getMaxMetricsRange("2026-09-24", "2026-09-24");

Returns

MaxMetricsResult — 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.

throws GarminError if start > end; passes through unchecked

Verification: live-verified

getMorningTrainingReadiness

garmin.getMorningTrainingReadiness(cdate: string | Date): Promise<TrainingReadinessEntry | null>
const result = await garmin.getMorningTrainingReadiness("2026-09-24");

Returns

TrainingReadinessEntry:

FieldTypeAlways present
inputContextstringno

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 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

Verification: live-verified

getPowerZones

garmin.getPowerZones(): Promise<PowerZoneEntry[] | null>
const result = await garmin.getPowerZones();

Returns

An array of PowerZoneEntry — 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

getPowerZonesForSport

garmin.getPowerZonesForSport(sport: string): Promise<PowerZonesForSportResult | null>
const result = await garmin.getPowerZonesForSport("sport");

Returns

PowerZonesForSportResult — 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.

sport upper-cased and validated the same way as getFunctionalThresholdPowerRange

Verification: live-verified

getRacePredictions

garmin.getRacePredictions(startdate?: string | Date, enddate?: string | Date, type?: "daily" | "monthly"): Promise<RacePredictionsResult | null>
const result = await garmin.getRacePredictions();

Returns

RacePredictionsResult — 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.

TWO branches, all-or-nothing params (throws on a partial combination), see gotchas: no params hits .../latest/{displayName}; all three hit .../{type}/{displayName}, capped at a 366-day span

Verification: live-verified

getRunningTolerance

garmin.getRunningTolerance(startdate: string | Date, enddate: string | Date, aggregation?: RunningToleranceAggregation): Promise<RunningToleranceEntry[] | null>
const result = await garmin.getRunningTolerance("2026-09-24", "2026-09-24");

Returns

An array of RunningToleranceEntry — 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 aggregation="weekly"; restricted to {daily,weekly} (narrower than the FTP/lactate methods); passes through unchecked

Verification: live-verified

getTrainingReadiness

garmin.getTrainingReadiness(cdate: string | Date): Promise<TrainingReadinessEntry[] | null>
const result = await garmin.getTrainingReadiness("2026-09-24");

Returns

An array of TrainingReadinessEntry:

FieldTypeAlways present
inputContextstringno

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

getTrainingStatus

garmin.getTrainingStatus(cdate: string | Date): Promise<TrainingStatusResult | null>
const result = await garmin.getTrainingStatus("2026-09-24");

Returns

TrainingStatusResult — 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

setHeartRateZones

garmin.setHeartRateZones(update: HeartRateZoneUpdate): Promise<HeartRateZoneEntry | null>
const result = await garmin.setHeartRateZones(update);

Returns

HeartRateZoneEntry:

FieldTypeAlways present
sportstringno
trainingMethodHeartRateZoneMethodno
zone1Floornumberno
zone2Floornumberno
zone3Floornumberno
zone4Floornumberno
zone5Floornumberno
maxHeartRateUsed`numbernull`
restingHeartRateUsed`numbernull`
lactateThresholdHeartRateUsed`numbernull`
restingHrAutoUpdateUsedbooleanno
changeStatestringno

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.

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 ascending"; checked here first)

Verification: live-verified

Methods in this category