Badges & challenges
Earned, available and in-progress badges, plus ad-hoc, badge and virtual challenges.
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);
9 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 |
|---|---|
getAdhocChallenges | live-verified |
getAvailableBadgeChallenges | live-verified |
getAvailableBadges | live-verified |
getBadgeChallenges | live-verified |
getBadgeDetail | live-verified |
getEarnedBadges | live-verified |
getInProgressBadges | live-verified |
getInprogressVirtualChallenges | live-verified |
getNonCompletedBadgeChallenges | live-verified |
getAdhocChallenges
garmin.getAdhocChallenges(start: number, limit: number): Promise<AdhocChallenge[] | null>
const result = await garmin.getAdhocChallenges(1, 1);
Returns
An array of AdhocChallenge — 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 /adhocchallenge-service/adHocChallenge/historical; start validated non-negative, limit validated positive (throws GarminError otherwise); passes through unchecked; returns an ARRAY, verified live
Verification: live-verified
getAvailableBadgeChallenges
garmin.getAvailableBadgeChallenges(start: number, limit: number): Promise<AvailableBadgeChallenge[] | null>
const result = await garmin.getAvailableBadgeChallenges(1, 1);
Returns
An array of AvailableBadgeChallenge — 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 /badgechallenge-service/badgeChallenge/available; same start/limit validation; passes through unchecked; same array-not-dict correction and same live start=0 -> 400 discovery as getBadgeChallenges
Verification: live-verified
getAvailableBadges
garmin.getAvailableBadges(): Promise<Badge[] | null>
const result = await garmin.getAvailableBadges();
Returns
An array of Badge:
| Field | Type | Always present |
|---|---|---|
badgeId | number | no |
badgeProgressValue | number | no |
badgeTargetValue | number | no |
badgeLimitCount | number | no |
badgeEarnedNumber | number | no |
badgeImageUrls | BadgeImageUrls | 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 /badge-service/badge/available?showExclusiveBadge=true; same badgeImageUrls as getEarnedBadges; stays nullable
Verification: live-verified
getBadgeChallenges
garmin.getBadgeChallenges(start: number, limit: number): Promise<BadgeChallenge[] | null>
const result = await garmin.getBadgeChallenges(1, 1);
Returns
An array of BadgeChallenge — 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 /badgechallenge-service/badgeChallenge/completed; same start/limit validation as getAdhocChallenges; passes through unchecked; 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 practice
Verification: live-verified
getBadgeDetail
garmin.getBadgeDetail(badgeId: number): Promise<BadgeDetail | null>
const result = await garmin.getBadgeDetail(activityId);
Returns
BadgeDetail
GETs /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 text — see the BadgeDetail type for where Garmin's web app gets it
Verification: live-verified
getEarnedBadges
garmin.getEarnedBadges(): Promise<Badge[] | null>
const result = await garmin.getEarnedBadges();
Returns
An array of Badge:
| Field | Type | Always present |
|---|---|---|
badgeId | number | no |
badgeProgressValue | number | no |
badgeTargetValue | number | no |
badgeLimitCount | number | no |
badgeEarnedNumber | number | no |
badgeImageUrls | BadgeImageUrls | 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 /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 [])
Verification: live-verified
getInProgressBadges
garmin.getInProgressBadges(): Promise<Badge[]>
const result = await garmin.getInProgressBadges();
Returns
An array of Badge:
| Field | Type | Always present |
|---|---|---|
badgeId | number | no |
badgeProgressValue | number | no |
badgeTargetValue | number | no |
badgeLimitCount | number | no |
badgeEarnedNumber | number | no |
badgeImageUrls | BadgeImageUrls | 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.
no 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)
Verification: live-verified
getInprogressVirtualChallenges
garmin.getInprogressVirtualChallenges(start: number, limit: number): Promise<InprogressVirtualChallenge[] | null>
const result = await garmin.getInprogressVirtualChallenges(1, 1);
Returns
An array of InprogressVirtualChallenge — 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 /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; passes through unchecked; same array-not-dict correction
Verification: live-verified
getNonCompletedBadgeChallenges
garmin.getNonCompletedBadgeChallenges(start: number, limit: number): Promise<NonCompletedBadgeChallenge[] | null>
const result = await garmin.getNonCompletedBadgeChallenges(1, 1);
Returns
An array of NonCompletedBadgeChallenge — 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 /badgechallenge-service/badgeChallenge/non-completed; same start/limit validation; passes through unchecked; same array-not-dict correction and same live start=0 -> 400 discovery as getBadgeChallenges
Verification: live-verified