This guide covers all breaking changes when upgrading from v2.x to v3.x. Every change here comes from fixing SDK routes that disagreed with the live Intervals.icu API — see AUDIT.md for the live verdicts behind each one.
| Removed method | Replacement |
|---|---|
client.fitness.getFitness() |
client.athletes.getSummary() |
client.fitness.getSummaries() |
client.athletes.getSummary() |
client.search.searchAthletes() |
none — the route never existed |
client.wellness.deleteWellness() |
none — the API has no DELETE mapping for wellness records |
client.activities.getWeather() |
removed; the nearest working endpoint is getWeatherSummary(), which returns a different shape (ActivityWeatherSummary) |
client.workouts.downloadWorkout() |
client.workouts.convertWorkout(), or client.events.downloadWorkout(eventId, format) for a calendar event |
client.workouts.downloadWorkoutForAthlete() |
client.workouts.convertWorkoutForAthlete(), or client.events.downloadWorkout(eventId, format) |
| Before | After |
|---|---|
client.chats.listChats() — called GET /chats (404) |
client.chats.listChats() — now calls GET /athlete/{id}/chats |
client.performance.getPowerVsHR() |
client.performance.getPowerHRCurve({ start, end }) — calls /power-hr-curve |
client.weather.getWeather() |
client.weather.getForecast() — calls /weather-forecast |
client.routes.getSimilarities(routeId) — returned a list |
client.routes.getSimilarity(routeId, otherRouteId) — returns a single RouteSimilarity |
client.activities.downloadFitFiles() — sent GET (405) |
client.activities.downloadFitFiles() — now sends POST |
client.activities.updateStreamsCSV() — sent POST (405) |
client.activities.updateStreamsCSV() — now sends PUT |
PowerModel fields cp, w_prime, p_max, ftp_watts, ftp_secs, r2 and its [key: string]: unknown index signature; PowerModelType = string |
PowerModel now has the API's actual fields type, criticalPower, wPrime, pMax, inputPointIndexes, ftp (the old names were never populated) and PowerModelType is the union 'MS_2P' | 'MORTON_3P' | 'FFT_CURVES' | 'ECP'. Read criticalPower instead of cp, etc.; there is no index signature, so cast if you must read an undocumented field. |
Note: client.activities.getPowerVsHR(activityId) is unrelated and unchanged — it's a per-activity endpoint that was always spec-valid, distinct from the removed client.performance.getPowerVsHR().
IHttpClient.download() signature changedownload(url, options?) replaces download(url, params?). This only matters if you implement a custom IHttpClient — the built-in AxiosHttpClient handles the change transparently:
// v2
download(url: string, params?: Record<string, unknown>): Promise<Buffer>;
// v3
interface DownloadOptions {
method?: 'GET' | 'POST';
params?: Record<string, unknown> | URLSearchParams; // URLSearchParams lets a key repeat, e.g. ids=a&ids=b
data?: unknown; // JSON body, only meaningful with POST
}
download(url: string, options?: DownloadOptions): Promise<Buffer>;
UploadConfig (used by upload()) gained the same idea: an optional method?: 'POST' | 'PUT', defaulting to POST.
// ─── Before (v2) ───
const fitness = await client.fitness.getFitness({ oldest: '2024-01-01', newest: '2024-12-31' });
const curve = await client.performance.getPowerVsHR();
const weather = await client.weather.getWeather();
const similar = await client.routes.getSimilarities(routeId);
const zwo = await client.workouts.downloadWorkout(workoutId, '.zwo');
// ─── After (v3) ───
const summary = await client.athletes.getSummary({ start: '2024-01-01', end: '2024-12-31' });
const curve = await client.performance.getPowerHRCurve({ start: '2024-01-01', end: '2024-12-31' });
const weather = await client.weather.getForecast();
const similar = await client.routes.getSimilarity(routeId, otherRouteId);
// convertWorkout POSTs a workout body instead of downloading an existing library
// workout by id. The body type, WorkoutConversionInput, requires `name`, `description`,
// `type` and `workout_doc`: live probes showed a body with all four converts and a body
// without `workout_doc` returns HTTP 500 (see AUDIT.md; fields were not probed one at a
// time). Every field on a calendar Event is optional, so narrow one before converting it:
import type { Event, WorkoutConversionInput } from '@0x3639/intervals-icu';
// Check runtime shapes, not just truthiness: the API response is external data.
const isConvertible = (e: Event): e is Event & WorkoutConversionInput & { id: number } =>
typeof e.id === 'number' &&
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: '2024-01-01', newest: '2024-12-31', category: ['WORKOUT'] });
const event = events.find(isConvertible);
if (event) {
const zwo = await client.workouts.convertWorkout(event, '.zwo');
// Or download an existing calendar event's workout file directly by id:
const zwo2 = await client.events.downloadWorkout(event.id, '.zwo');
}
This guide covers all breaking changes when upgrading from v1.x to v2.x.
All top-level facade methods on IntervalsClient have been removed. Use the service accessors instead:
| Removed method | Replacement |
|---|---|
client.getAthlete() |
client.athletes.getAthlete() |
client.updateAthlete(data) |
client.athletes.updateAthlete(data) |
client.getSportSettings() |
client.sportSettings.list() |
client.getEvents(opts) |
client.events.listEvents(opts) |
client.getEvent(id) |
client.events.getEvent(id) |
client.createEvent(data) |
client.events.createEvent(data) |
client.updateEvent(id, data) |
client.events.updateEvent(id, data) |
client.deleteEvent(id) |
client.events.deleteEvent(id) |
client.getWellness(opts) |
client.wellness.listWellness(opts) |
client.createWellness(data) |
client.wellness.createWellness(data) |
client.updateWellness(date, data) |
client.wellness.updateWellness(date, data) |
client.deleteWellness(date) |
client.wellness.deleteWellness(date) |
client.getWorkouts(opts) |
client.workouts.listWorkouts(opts) |
client.getWorkout(id) |
client.workouts.getWorkout(id) |
client.createWorkout(data) |
client.workouts.createWorkout(data) |
client.updateWorkout(id, data) |
client.workouts.updateWorkout(id, data) |
client.deleteWorkout(id) |
client.workouts.deleteWorkout(id) |
client.getActivities(opts) |
client.activities.listActivities(opts) |
client.getActivity(id) |
client.activities.getActivity(id) |
client.updateActivity(id, data) |
client.activities.updateActivity(id, data) |
client.deleteActivity(id) |
client.activities.deleteActivity(id) |
The following deprecated service-level aliases were also removed:
| Removed method | Replacement |
|---|---|
activityService.getActivities() |
activityService.listActivities() |
eventService.getEvents() |
eventService.listEvents() |
wellnessService.getWellness() |
wellnessService.listWellness() |
workoutService.getWorkouts() |
workoutService.listWorkouts() |
athleteService.getSportSettings() |
sportSettingsService.list() |
Activity IDs are now string (e.g. 'i55610271'), matching the Intervals.icu API. The facade previously accepted number | string for backward compatibility — that shim is gone.
// v1.x
const activity = await client.getActivity(12345);
// v2
const activity = await client.activities.getActivity('i12345');
The default request timeout changed from 10 seconds to 30 seconds.
apiKey is now optional. You must provide either apiKey (for personal API key auth) or accessToken (for OAuth bearer token auth):
// API key
const client = new IntervalsClient({ apiKey: 'your-key' });
// OAuth
const client = new IntervalsClient({ accessToken: 'oauth-token' });
The default athleteId changed from 'me' to '0'. Both resolve to the authenticated athlete on the Intervals.icu API, so this is effectively a no-op.
Single-activity endpoints now correctly use /activity/{id} instead of the previous incorrect /athlete/{athleteId}/activities/{id}. This only matters if you were relying on URL interception or mocking.
// ─── Before (v1.x) ───
import { IntervalsClient } from 'intervals-icu';
const client = new IntervalsClient({ apiKey: 'key', athleteId: 'me' });
const athlete = await client.getAthlete();
const events = await client.getEvents({ oldest: '2024-01-01', newest: '2024-12-31' });
const activity = await client.getActivity(12345);
await client.createWellness({ date: '2024-01-15', weight: 70 });
const settings = await client.getSportSettings();
// ─── After (v2) ───
import { IntervalsClient } from 'intervals-icu';
const client = new IntervalsClient({ apiKey: 'key' });
const athlete = await client.athletes.getAthlete();
const events = await client.events.listEvents({ oldest: '2024-01-01', newest: '2024-12-31' });
const activity = await client.activities.getActivity('i12345');
await client.wellness.createWellness({ date: '2024-01-15', weight: 70 });
const settings = await client.sportSettings.list();