client.analytics computes per-activity histograms (power, heart rate, pace, GAP), time-at-heart-rate, interval statistics and power models, plus an athlete-level mean-maximal power (MMP) model used to resolve %MMP workout steps. Activity ids are URL-encoded before use; the older single-curve activity methods remain on ActivityService.
| Method | What it does |
|---|---|
| AnalyticsService.getPowerHistogram | Time in power buckets |
| AnalyticsService.getHRHistogram | Time in heart-rate buckets |
| AnalyticsService.getPaceHistogram | Time in pace buckets |
| AnalyticsService.getGAPHistogram | Time in grade-adjusted-pace buckets |
| AnalyticsService.getTimeAtHR | Seconds spent at each heart rate |
| AnalyticsService.getIntervalStats | Statistics for an arbitrary index range of the activity, as if it were an interval |
| AnalyticsService.getPowerSpikeModel | Power model fitted to the activity, used to detect power spikes |
| AnalyticsService.getCurves | Curves (watts, pace, HR, etc.) for one activity, optionally with fatigued variants |
| AnalyticsService.getCurvesCSV | Same as getCurves, as CSV |
| AnalyticsService.getMMPModel | Power model used to resolve %MMP workout steps for a sport type |
For the newest typed activity in the last year, fetch its power histogram, HR histogram and time-at-heart-rate plot. The API returns HTTP 422 when a requested stream is not available for the activity; each call is wrapped separately so one missing stream does not stop the others.
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!;
try {
const powerBuckets = await client.analytics.getPowerHistogram(id, { bucketSize: 50 });
console.log(`Power histogram: ${powerBuckets.length} bucket(s)`);
} catch (err) {
if (err instanceof IntervalsAPIError && err.status === 422) {
console.log(`Power histogram not available for ${id}: status=${err.status}`);
} else {
throw err;
}
}
try {
const hrBuckets = await client.analytics.getHRHistogram(id);
console.log(`HR histogram: ${hrBuckets.length} bucket(s)`);
} catch (err) {
if (err instanceof IntervalsAPIError && err.status === 422) {
console.log(`HR histogram not available for ${id}: status=${err.status}`);
} else {
throw err;
}
}
try {
const timeAtHR = await client.analytics.getTimeAtHR(id);
console.log(`Time at HR: ${timeAtHR.secs?.length ?? 0} bpm bucket(s)`);
} catch (err) {
if (err instanceof IntervalsAPIError && err.status === 422) {
console.log(`Time at HR not available for ${id}: status=${err.status}`);
} else {
throw err;
}
}
}
Fetch the athlete's ride power model, then for the newest typed activity in the last year: its power spike model, its watts curve (normal fatigue only), and interval stats for its first detected interval, if any. AnalyticsService.getMMPModel field names ( PowerModel) follow the spec's PowerModel schema.
const client = new IntervalsClient({ apiKey });
const mmpModel = await client.analytics.getMMPModel('Ride');
console.log(`MMP model: type=${mmpModel.type ?? 'n/a'} criticalPower=${mmpModel.criticalPower ?? 'n/a'} wPrime=${mmpModel.wPrime ?? 'n/a'} pMax=${mmpModel.pMax ?? 'n/a'}`);
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!;
try {
const spikeModel = await client.analytics.getPowerSpikeModel(id);
console.log(`Power spike model: type=${spikeModel.type ?? 'n/a'} criticalPower=${spikeModel.criticalPower ?? 'n/a'}`);
} catch (err) {
if (err instanceof IntervalsAPIError && err.status === 422) {
console.log(`Power spike model not available for ${id}: status=${err.status}`);
} else {
throw err;
}
}
try {
const curves = await client.analytics.getCurves(id, { types: ['watts'], fatigue: ['normal'] });
console.log(`Curves: ${curves.length}`);
} catch (err) {
if (err instanceof IntervalsAPIError && err.status === 422) {
console.log(`Curves not available for ${id}: status=${err.status}`);
} else {
throw err;
}
}
const intervals = await client.activities.getIntervals(id);
const firstInterval = intervals.icu_intervals?.[0];
if (!firstInterval || firstInterval.start_index === undefined || firstInterval.end_index === undefined) {
console.log('No detected intervals with an index range');
} else {
try {
const stats = await client.analytics.getIntervalStats(id, firstInterval.start_index, firstInterval.end_index);
console.log(`Interval stats: average_watts=${stats.average_watts ?? 'n/a'} average_heartrate=${stats.average_heartrate ?? 'n/a'}`);
} catch (err) {
if (err instanceof IntervalsAPIError && err.status === 422) {
console.log(`Interval stats not available for ${id}: status=${err.status}`);
} else {
throw err;
}
}
}
}
types: ['watts'] with fatigue: ['normal'] is confirmed to succeed on the live test account.