Every method that downloads a file resolves with a Node Buffer, ready to write to disk, pipe to a stream, or inspect directly:
| Method | Format |
|---|---|
| ActivityService.downloadFile | Original uploaded activity file (gzip compressed) |
| ActivityService.downloadFitFile | Intervals.icu-generated FIT file for one activity |
| ActivityService.downloadFitFiles | Zip of Intervals.icu-generated FIT files for several activities |
| ActivityService.downloadGPX | GPX file, optionally with power/heart-rate extensions |
| ActivityService.downloadActivitiesCSV | All activities as CSV |
| ActivityService.getStreamsCSV | One activity's streams as CSV |
| EventService.downloadWorkout | One calendar workout in zwo, mrc, erg or fit format |
| EventService.downloadWorkoutsZip | Calendar workouts in a date range, zipped |
| WorkoutService.convertWorkout | A workout definition converted to zwo, mrc, erg or fit |
| WorkoutService.convertWorkoutForAthlete | Same, resolved against an athlete's own FTP/zones |
| GearService.downloadCSV | Gear list as CSV |
| AnalyticsService.getCurvesCSV | Activity power curves as CSV |
| PerformanceService.getActivityPaceCurvesCSV | Athlete-level pace curves as CSV |
const client = new IntervalsClient({ apiKey });
const localDate = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const now = new Date(); // one timestamp for both bounds
const yearAgo = new Date(now);
yearAgo.setFullYear(yearAgo.getFullYear() - 1);
const newest = localDate(now);
const oldest = localDate(yearAgo);
const activities = await client.activities.listActivities({ oldest, newest });
const activity = activities.find((a) => a.type);
if (!activity) {
console.log('No activities with a type in the last year');
} else {
const id = activity.id!;
const fit = await client.activities.downloadFitFile(id);
// A fresh directory with a random name, rather than a predictable path in the shared
// temp directory: nothing else can pre-create or read the file. `wx` refuses to write
// to an existing path and 0o600 keeps the file readable only by the current user.
const outDir = mkdtempSync(join(tmpdir(), 'intervals-icu-'));
const fitPath = join(outDir, `${id}.fit`);
writeFileSync(fitPath, fit, { mode: 0o600, flag: 'wx' });
console.log(`FIT file: ${fit.length} bytes, written to ${fitPath} (left in place for inspection)`);
const gpx = await client.activities.downloadGPX(id);
console.log(`GPX file: ${gpx.length} bytes`);
}
const csv = await client.activities.downloadActivitiesCSV();
console.log(`Activities CSV: ${csv.length} bytes`);
Uploads take a Buffer | Blob | Uint8Array plus a filename, and are sent as multipart/form-data:
CI never runs this example; running it by hand changes the authenticated account: it uploads a real activity and deletes it again in a finally block. If the upload response carries no activity id, the activity stays on the account and has to be deleted by hand. If the create call itself fails after the server committed it, nothing is cleaned up: search the account for Created by the SDK example (or Uploaded by the SDK example) and delete it by hand.
// maxRetries: 0 — a create that committed before a 5xx would be repeated by a retry, and only the last response's id would be cleaned up.
const client = new IntervalsClient({ apiKey, maxRetries: 0 });
// A unique marker in the name: if the upload fails after the server committed it, this
// is what to search the account for.
const marker = `Uploaded by the SDK example ${Date.now()}`;
const file = readFileSync(filePath);
const uploaded = await client.activities.uploadActivity(file, basename(filePath), {
name: marker,
});
// UploadResponse reports the created activities in `activities`, and a single-activity
// upload also echoes an `id`. Collect whichever is present so the example can clean up
// after itself.
const created = (uploaded.activities ?? []).flatMap((a) => (typeof a.id === 'string' ? [a.id] : []));
if (created.length === 0 && typeof uploaded.id === 'string') created.push(uploaded.id);
try {
console.log(`Uploaded, id=${created.join(', ') || 'not reported in the response'}`);
} finally {
for (const id of created) {
try {
await client.activities.deleteActivity(id);
console.log(`Deleted activity ${id}`);
} catch (err) {
console.error(`Could not delete activity ${id}; remove it by hand:`, err);
}
}
if (created.length === 0) {
console.log('The upload response carried no activity id; the uploaded activity remains on the account');
}
}
WorkoutService.convertWorkout and WorkoutService.convertWorkoutForAthlete take a WorkoutConversionInput body and a target WorkoutFormat. All four of name, description, type and workout_doc are required on that type, not just workout_doc: live probes recorded in AUDIT.md found that a body missing workout_doc gets HTTP 500 from the API, and the remaining fields were not probed individually, so the SDK treats all four as required as a conservative contract. A calendar workout event's own workout_doc field (from EventService.listEvents) is a convenient source for a real body, since a library or calendar workout already carries all four fields together. See API behaviour for the probe record.
const client = new IntervalsClient({ apiKey });
const localDate = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const now = new Date(); // one timestamp for both bounds
const yearAgo = new Date(now);
yearAgo.setFullYear(yearAgo.getFullYear() - 1);
const newest = localDate(now);
const oldest = localDate(yearAgo);
// convertWorkout() requires name, description, type and workout_doc; check the runtime
// shape of each rather than trusting the Event type, since API data is external.
type ConvertibleEvent = Event & WorkoutConversionInput;
const isConvertible = (e: Event): e is ConvertibleEvent =>
typeof e.name === 'string' &&
typeof e.description === 'string' &&
typeof e.type === 'string' &&
typeof e.workout_doc === 'object' && e.workout_doc !== null && !Array.isArray(e.workout_doc);
const events = await client.events.listEvents({ oldest, newest, category: ['WORKOUT'] });
const event = events.find(isConvertible);
if (!event) {
console.log('No calendar workouts found');
} else {
const zwo = await client.workouts.convertWorkout(
{ name: event.name, description: event.description, type: event.type, workout_doc: event.workout_doc },
'.zwo',
);
console.log(zwo.toString('utf8').slice(0, 80));
}
const zip = await client.events.downloadWorkoutsZip({ ext: '.zwo', oldest, newest });
console.log(`Workouts zip: ${zip.length} bytes`);
Two related format details:
ext query parameter without the leading dot (zwo, not .zwo), even though WorkoutFormat itself (used elsewhere, such as the {ext} path suffix on convertWorkout) includes the dot. The SDK strips it before sending the request.distances array. The JSON form of the same route ( PerformanceService.getActivityPaceCurves) accepts the parameter being omitted, but the CSV form returns HTTP 500 without at least one distance — an empty array is dropped from the query string entirely, which reproduces the same 500, so the type requires at least one element. See API behaviour for the probe record.