Courses
Saved routes you can send to a device and follow: import a GPX, create, rename, change privacy, export as GPX, delete. Creating is two steps — importCourseGpx parses, createCourse saves — and createCourseFromGpx does both.
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);
8 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 |
|---|---|
createCourse | live-verified |
createCourseFromGpx | live-verified |
deleteCourse | live-verified |
downloadCourseGpx | live-verified |
getCourse | live-verified |
importCourseGpx | live-verified |
listCourses | live-verified |
updateCourse | live-verified |
createCourse
garmin.createCourse(input: CourseInput): Promise<Course | null>
const result = await garmin.createCourse(input);
Returns
Course:
| Field | Type | Always present |
|---|---|---|
courseId | `number | null` |
courseName | `string | null` |
description | `string | null` |
rulePK | `number | null` |
activityTypePk | `number | null` |
distanceMeter | `number | null` |
elevationGainMeter | `number | null` |
elevationLossMeter | `number | null` |
geoPoints | CourseGeoPoint[] | yes |
coursePoints | CoursePoint[] | 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.
POSTs /course-service/course with the template Garmin's web client sends. CourseInput = { name, geoPoints, coursePoints?, activityTypeId? = 1 (running), privacy? = "private" }; privacy maps to rulePK public 1 / private 2. Garmin computes distanceMeter and elevation. Throws GarminError before any request on an empty name or fewer than two geoPoints
Verification: live-verified
createCourseFromGpx
garmin.createCourseFromGpx(file: Blob, filename: string, options?: { name?, activityTypeId?, privacy? }): Promise<Course | null>
const result = await garmin.createCourseFromGpx(file, "filename");
Returns
Course:
| Field | Type | Always present |
|---|---|---|
courseId | `number | null` |
courseName | `string | null` |
description | `string | null` |
rulePK | `number | null` |
activityTypePk | `number | null` |
distanceMeter | `number | null` |
elevationGainMeter | `number | null` |
elevationLossMeter | `number | null` |
geoPoints | CourseGeoPoint[] | yes |
coursePoints | CoursePoint[] | 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.
importCourseGpx then createCourse. Name defaults to the GPX <name>, then the filename without .gpx
Verification: live-verified
deleteCourse
garmin.deleteCourse(courseId: number | string): Promise<unknown>
const result = await garmin.deleteCourse(activityId);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
DELETE /course-service/course/{courseId}, resolves null (204). IRREVERSIBLE. May 429 "not yet ready" for a few seconds after creation
Verification: live-verified
downloadCourseGpx
garmin.downloadCourseGpx(courseId: number | string): Promise<Buffer>
const result = await garmin.downloadCourseGpx(activityId);
Returns
A Buffer of file bytes.
GETs /course-service/course/gpx/{courseId} through client.download
Verification: live-verified
getCourse
garmin.getCourse(courseId: number | string): Promise<Course | null>
const result = await garmin.getCourse(activityId);
Returns
Course:
| Field | Type | Always present |
|---|---|---|
courseId | `number | null` |
courseName | `string | null` |
description | `string | null` |
rulePK | `number | null` |
activityTypePk | `number | null` |
distanceMeter | `number | null` |
elevationGainMeter | `number | null` |
elevationLossMeter | `number | null` |
geoPoints | CourseGeoPoint[] | yes |
coursePoints | CoursePoint[] | 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.
GETs /course-service/course/{courseId}; a missing id is a 404 GarminHttpError ("Course not found : {id}")
Verification: live-verified
importCourseGpx
garmin.importCourseGpx(file: Blob, filename: string): Promise<Course>
const result = await garmin.importCourseGpx(file, "filename");
Returns
Course:
| Field | Type | Always present |
|---|---|---|
courseId | `number | null` |
courseName | `string | null` |
description | `string | null` |
rulePK | `number | null` |
activityTypePk | `number | null` |
distanceMeter | `number | null` |
elevationGainMeter | `number | null` |
elevationLossMeter | `number | null` |
geoPoints | CourseGeoPoint[] | yes |
coursePoints | CoursePoint[] | 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.
multipart POST (field file) to /course-service/course/import. Parses only, saves NOTHING: the result has courseId: null and a RESAMPLED track (13 GPX points came back as 37) with null elevations/timestamps; courseName is the GPX <name>. Only .gpx accepted (only .gpx verified); throws GarminError on an empty response
Verification: live-verified
listCourses
garmin.listCourses(): Promise<CourseList | null>
const result = await garmin.listCourses();
Returns
CourseList:
| Field | Type | Always present |
|---|---|---|
coursesForUser | CourseSummary[] | 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.
GETs /web-gateway/course/owner/ (trailing slash included); returns an ENVELOPE { coursesForUser: CourseSummary[] }, not a bare array
Verification: live-verified
updateCourse
garmin.updateCourse(courseId: number | string, changes: { name?, privacy? }): Promise<Course | null>
const result = await garmin.updateCourse(activityId, changes);
Returns
Course:
| Field | Type | Always present |
|---|---|---|
courseId | `number | null` |
courseName | `string | null` |
description | `string | null` |
rulePK | `number | null` |
activityTypePk | `number | null` |
distanceMeter | `number | null` |
elevationGainMeter | `number | null` |
elevationLossMeter | `number | null` |
geoPoints | CourseGeoPoint[] | yes |
coursePoints | CoursePoint[] | 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.
READ-MODIFY-WRITE: GETs the record, overlays the changes, PUTs the whole record back to /course-service/course/{courseId} — the same request Garmin's own web editor sends on Save (observed 2026-09-24). Concurrent callers can clobber each other. Throws before any request if neither field is given or the name is blank. A just-created course may 429 "not yet ready" (see gotchas)
Verification: live-verified