client.performance computes athlete-level power, pace and heart-rate curves (best efforts over time) and compares those curves across a set of activities. Single-activity curves remain on ActivityService.
| Method | What it does |
|---|---|
| PerformanceService.getPowerCurves | Get athlete-level power curves (best efforts over time) |
| PerformanceService.getActivityPowerCurves | Compare power curves across activities |
| PerformanceService.getPaceCurves | Get athlete-level pace curves |
| PerformanceService.getHRCurves | Get athlete-level HR curves |
| PerformanceService.getActivityHRCurves | Compare HR curves across activities |
| PerformanceService.getActivityPaceCurves | Best pace over a set of distances across the activities in a date range |
| PerformanceService.getActivityPaceCurvesCSV | Same as getActivityPaceCurves, as CSV |
| PerformanceService.getPowerHRCurve | Get the athlete's power vs heart rate curve for a date range |
Fetch the athlete's power, pace and HR curves for the last year, and the power-vs-heart-rate curve over the same range. The three curve routes choose their window with CurveOptions.curves, not with a date range: each entry is 1y/2y (the past year, the past two years…), 42d (the past 42 days…), s0/s1 (the current season, the previous one…), all, or r.2026-01-01.2026-03-31 for an explicit range, optionally with a -kj0/-kj1 suffix for the fatigued curve. type is required by PerformanceService.getPowerCurves — PowerCurveOptions makes it so, and the API returns HTTP 422 without it — while PerformanceService.getPaceCurves ( PaceCurveOptions, which adds gap) and PerformanceService.getHRCurves ( CurveOptions) accept it but work without it. PerformanceService.getPowerHRCurve is the exception: it takes an explicit start/end date range.
const client = new IntervalsClient({ apiKey });
// The curve routes pick their window with `curves`, not a date range: '1y' is the past
// year, '42d' the past 42 days, 's0' the current season, 'all' all time, and
// 'r.2026-01-01.2026-03-31' an explicit range. The API requires `type` for power-curves
// (HTTP 422 without it); pace-curves and hr-curves work without it.
const powerCurves = await client.performance.getPowerCurves({ curves: ['1y'], type: 'Ride' });
console.log(`Power curves: ${powerCurves.list?.length ?? 0}`);
const paceCurves = await client.performance.getPaceCurves({ curves: ['1y'] });
console.log(`Pace curves: ${paceCurves.list?.length ?? 0}`);
const hrCurves = await client.performance.getHRCurves({ curves: ['1y'] });
console.log(`HR curves: ${hrCurves.list?.length ?? 0}`);
// getPowerHRCurve is the exception: it takes an explicit start/end date range. The API
// keys dates by the athlete's local date, not the machine's, so the bounds are formatted
// in the athlete's time zone (see examples/_shared/local-date.ts).
const me = await client.athletes.getAthlete();
const now = new Date(); // one timestamp for both bounds
const yearAgo = new Date(now);
yearAgo.setFullYear(yearAgo.getFullYear() - 1);
const powerHR = await client.performance.getPowerHRCurve({
start: localDateIn(yearAgo, me.timezone),
end: localDateIn(now, me.timezone),
});
console.log(`Power vs HR: ${powerHR.bpm?.length ?? 0} point(s)`);
Best pace over a set of distances across the athlete's runs in the last year, as JSON and as CSV. distances is required for the CSV form: the API returns HTTP 500 without it, while the JSON form accepts the omission. The element shape of ActivityPaceCurves.curves is unobserved on the live account (it was empty for every sport tried), so the example prints what comes back rather than any specific fields. This route also does not accept the SDK's default "current athlete" alias (athlete id 0); the example resolves a real athlete id first.
const client = new IntervalsClient({ apiKey });
const me = await client.athletes.getAthlete();
if (!me.id) {
console.error('Could not resolve the authenticated athlete id.');
process.exit(1);
}
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);
// getActivityPaceCurvesCSV's `distances` parameter is typed as a non-empty tuple
// ([number, ...number[]]), not a plain number[], so it is cast to match.
const options = { oldest, newest, type: 'Run' as const, distances: [1000, 5000] as [number, ...number[]] };
const curves = await client.performance.getActivityPaceCurves(options, me.id);
console.log(`distances=${curves.distances?.join(',') ?? 'n/a'} gap=${curves.gap ?? 'n/a'} curves=${curves.curves?.length ?? 0}`);
const csv = await client.performance.getActivityPaceCurvesCSV(options, me.id);
console.log(`CSV: ${csv.length} byte(s)`);
distances array; the JSON form ( PerformanceService.getActivityPaceCurves) does not.0; the example resolves the real id with athletes.getAthlete() first and passes athleteId explicitly. See the API behaviour row for GET /athlete/{id}/activity-pace-curves.f1, f2 and f3 comparison-filter arrays as required on all three curve routes, but the API answers without them, so the option types omit them. See the API behaviour row for GET /athlete/{id}/power-curves.ActivityPaceCurves.curves was not observed on the test account, so the example prints what comes back rather than any specific fields.