Skip to content
Docs menu

Getting started

About

garminconnect-js fetches your health, fitness, and activity data from connect.garmin.com so you can use it in your own Node or Next.js server code, without scraping HTML or reverse-engineering the mobile app yourself.

It covers wellness, activities, training metrics, workouts, gear, courses, devices, badges, body composition, women's health, golf, nutrition and training plans — see API coverage for the breakdown. For anything it doesn't wrap, client.connectapi() calls any Garmin Connect endpoint with the same auth.

Compatibility: requires Node.js 18+. This library is server-only — see Node runtime only below for why. It has no runtime dependencies and does not run in a browser or on an Edge runtime.

Status: 0.x, so the public API may still change between minor versions; breaking changes are listed in CHANGELOG.md. The endpoints underneath are undocumented and belong to Garmin, who can change them at any time — which is why every method carries a verification date rather than an assurance.

Garmin Connect has no public, documented API. Everything here talks to the same endpoints connect.garmin.com and the Garmin Connect mobile app use, so Garmin can change or break any of it without notice. Don't build anything safety-critical on it, and expect to update when login stops working.

Garmin returns 412 PreconditionFailedException: "The user is from EU location, but upload consent is not yet granted or revoked" for every write (addWeighIn, importActivity, createManualActivity, workout uploads, gear writes — all of them) on an EU-region account that has not clicked through Garmin Connect's upload-consent flow. This was hit live during this project's own development. It is an account-state precondition, not a library bug: the request shape and URL are correct, and Garmin's server is refusing the write until the account owner grants consent in Garmin Connect's own settings UI. No method in this library wraps Garmin's consent-status check, but you can read it directly:

// Response shape below is LIVE-OBSERVED, not assumed. There is no `enabled` field:
// the signal is `userOption`, which reads "opt-in" once consent has been granted.
const consent = await client.connectapi<{ userOption?: string }>(
  "/gdprconsent-service/feature/UPLOAD",
);
const uploadConsentGranted = consent?.userOption === "opt-in";

If your first write against an EU account 412s, check this before assuming your request is wrong.

Installation & setup

npm install garminconnect-js
import { GarminClient, Garmin, FileTokenStore } from "garminconnect-js";

const client = new GarminClient({ tokenStore: new FileTokenStore("./tokens") });
const result = await client.login(process.env.GARMIN_EMAIL!, process.env.GARMIN_PASSWORD!);

if (result.state === "mfa_required") {
  // login() does not throw or block on MFA — finish it with the code Garmin sent.
  await client.resumeLogin(result.mfaState, await promptForCode());
}

const garmin = new Garmin(client);
console.log(await garmin.getUserProfile());

(promptForCode is yours to supply — stdin in a script, a second HTTP request in a web app; see MFA across two HTTP requests.) Tokens are now in ./tokens, so a later process skips the password entirely:

const client = new GarminClient({ tokenStore: new FileTokenStore("./tokens") });
if (!(await client.loadTokens())) throw new Error("Not connected to Garmin — log in first");
const garmin = new Garmin(client);

loadTokens() returns false rather than throwing when the store is empty.

Client options

new GarminClient(options?) accepts:

OptionDefaultWhat it does
tokenStorenew MemoryTokenStore()Where tokens are loaded from and saved to. See Authentication.
isCnfalsetrue routes every request to garmin.cn for accounts registered in China.
timeoutMs10000Per-request timeout for ordinary JSON calls. download()/upload() default to 60s; every call can override it with { timeoutMs }.
retries3Retries after the first attempt, on a network error or a 408/500/502/503/504. POST is never retried, so a write cannot be duplicated.
backoffMs500Base delay between retries, doubling each attempt (500, 1000, 2000 ms).
fetchImplglobalThis.fetchSwap in your own fetch — for tests (see Testing), a proxy, or instrumentation.
loginDelayMsrandom 3000–8000Pause before the SSO widget's credential POST, used only when the mobile login is rate limited (see Authentication). Set 0 in tests.