Calendar events
Races and other dated events on the Garmin Connect calendar: list, read, create, update, delete. Events you create are always private.
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);
6 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 |
|---|---|
createCalendarEvent | live-verified |
deleteCalendarEvent | live-verified |
getCalendarEvent | live-verified |
getSharedCalendarEvent | live-verified |
listCalendarEvents | live-verified |
updateCalendarEvent | live-verified |
createCalendarEvent
garmin.createCalendarEvent(input: CalendarEventInput): Promise<CalendarEvent | null>
const result = await garmin.createCalendarEvent(input);
Returns
CalendarEvent:
| Field | Type | Always present |
|---|---|---|
id | number | yes |
eventName | string | yes |
date | string | yes |
url | `string | null` |
registrationUrl | `string | null` |
courseId | `number | null` |
completionTarget | `CalendarEventTarget | null` |
eventTimeLocal | `{ startTimeHhMm: string; timeZoneId: string } | null` |
note | `string | null` |
workoutId | `number | null` |
location | `string | null` |
eventType | `string | null` |
eventPrivacy | { label: string; isShareable: boolean; isDiscoverable: boolean } | no |
shareableEventUuid | `string | null` |
eventCustomization | { | no |
customGoal | `CalendarEventTarget | null` |
isPrimaryEvent | boolean | no |
isTrainingEvent | boolean | no |
associatedWithActivityId | `number | null` |
isGoalMet | `boolean | null` |
trainingPlanId | `number | null` |
race | boolean | 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.
POSTs /calendar-service/event. CalendarEventInput = {name, date, startTime?: "HH:MM", timeZoneId?, eventType?, race?, primary?, distance?: {value, unit: meter|kilometer|yard|mile}, goalTimeSeconds?, location?, url?, note?}; only name/date are required by Garmin ('addEvent.arg3.eventName' must not be blank otherwise). Garmin forces every user-created event PRIVATE and unshareable whatever is sent, sets isTrainingEvent whenever isPrimaryEvent is, and answers an unknown eventType with a 500. It also rewrote Europe/Zagreb to Europe/Paris (same offset) while keeping America/New_York as sent
Verification: live-verified
deleteCalendarEvent
garmin.deleteCalendarEvent(eventId: number | string): Promise<unknown>
const result = await garmin.deleteCalendarEvent(activityId);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
DELETE /calendar-service/event/{id}, resolves null (204). IRREVERSIBLE
Verification: live-verified
getCalendarEvent
garmin.getCalendarEvent(eventId: number | string): Promise<CalendarEvent | null>
const result = await garmin.getCalendarEvent(activityId);
Returns
CalendarEvent:
| Field | Type | Always present |
|---|---|---|
id | number | yes |
eventName | string | yes |
date | string | yes |
url | `string | null` |
registrationUrl | `string | null` |
courseId | `number | null` |
completionTarget | `CalendarEventTarget | null` |
eventTimeLocal | `{ startTimeHhMm: string; timeZoneId: string } | null` |
note | `string | null` |
workoutId | `number | null` |
location | `string | null` |
eventType | `string | null` |
eventPrivacy | { label: string; isShareable: boolean; isDiscoverable: boolean } | no |
shareableEventUuid | `string | null` |
eventCustomization | { | no |
customGoal | `CalendarEventTarget | null` |
isPrimaryEvent | boolean | no |
isTrainingEvent | boolean | no |
associatedWithActivityId | `number | null` |
isGoalMet | `boolean | null` |
trainingPlanId | `number | null` |
race | boolean | 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 /calendar-service/event/{id}; a missing id is a 404 GarminHttpError
Verification: live-verified
getSharedCalendarEvent
garmin.getSharedCalendarEvent(shareableEventUuid: string): Promise<CalendarEvent | null>
const result = await garmin.getSharedCalendarEvent(activityId);
Returns
CalendarEvent:
| Field | Type | Always present |
|---|---|---|
id | number | yes |
eventName | string | yes |
date | string | yes |
url | `string | null` |
registrationUrl | `string | null` |
courseId | `number | null` |
completionTarget | `CalendarEventTarget | null` |
eventTimeLocal | `{ startTimeHhMm: string; timeZoneId: string } | null` |
note | `string | null` |
workoutId | `number | null` |
location | `string | null` |
eventType | `string | null` |
eventPrivacy | { label: string; isShareable: boolean; isDiscoverable: boolean } | no |
shareableEventUuid | `string | null` |
eventCustomization | { | no |
customGoal | `CalendarEventTarget | null` |
isPrimaryEvent | boolean | no |
isTrainingEvent | boolean | no |
associatedWithActivityId | `number | null` |
isGoalMet | `boolean | null` |
trainingPlanId | `number | null` |
race | boolean | 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 /calendar-service/event/{uuid}/shareable: an event from Garmin's public events catalogue (Races & Events -> Find an Event), with locationStartPoint, subscribersCount, provider and subscribed. Works WITHOUT subscribing (subscribed: false). The plain /event/{uuid} is a 404. Events you create never get a shareable uuid (Garmin forces them private)
Verification: live-verified
listCalendarEvents
garmin.listCalendarEvents(startdate?: string | Date, enddate?: string | Date): Promise<CalendarEvent[] | null>
const result = await garmin.listCalendarEvents();
Returns
An array of CalendarEvent:
| Field | Type | Always present |
|---|---|---|
id | number | yes |
eventName | string | yes |
date | string | yes |
url | `string | null` |
registrationUrl | `string | null` |
courseId | `number | null` |
completionTarget | `CalendarEventTarget | null` |
eventTimeLocal | `{ startTimeHhMm: string; timeZoneId: string } | null` |
note | `string | null` |
workoutId | `number | null` |
location | `string | null` |
eventType | `string | null` |
eventPrivacy | { label: string; isShareable: boolean; isDiscoverable: boolean } | no |
shareableEventUuid | `string | null` |
eventCustomization | { | no |
customGoal | `CalendarEventTarget | null` |
isPrimaryEvent | boolean | no |
isTrainingEvent | boolean | no |
associatedWithActivityId | `number | null` |
isGoalMet | `boolean | null` |
trainingPlanId | `number | null` |
race | boolean | 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.
Events also appear in getScheduledWorkouts' month feed as itemType event; this GETs /calendar-service/events, with startDate/endDate when both dates are given (inclusive) or no params for every event on the account; one date alone throws
Verification: live-verified
updateCalendarEvent
garmin.updateCalendarEvent(eventId: number | string, changes: CalendarEventUpdate): Promise<unknown>
const result = await garmin.updateCalendarEvent(activityId, changes);
Returns
unknown — Garmin's response is passed through unparsed. Cast it to whatever you need; this library does not model it.
READ-MODIFY-WRITE: GETs the event, overlays {name?, date?, location?, url?, note?, race?, goalTimeSeconds?} (null clears an optional one), PUTs the whole record back to /calendar-service/event/{id}; Garmin answers with no body, so it resolves to null. Concurrent callers can clobber each other
Verification: live-verified