@0x3639/intervals-icu
    Preparing search index...

    Dates, pagination and arrays

    A local calendar date string in YYYY-MM-DD form works on every date-range parameter in the SDK (oldest, newest, start, end, and similar), and it is what all the examples here use. Both bounds are inclusive, and the date is read in the athlete's own time zone, so build it from the athlete's timezone rather than the machine's when the exact day matters.

    A few parameters accept more than a bare date. spec/openapi.json marks start and end on GET /athlete/{id}/athlete-summary{ext} — the route behind AthleteService.getSummary, in both its JSON and .csv forms — as "Local date and optional time (ISO-8601)", so those also take a local date-time such as 2026-09-24T06:00:00. They are the only parameters the spec describes that way; every other date-range parameter is documented as a plain local date.

    What never works is toISOString() output. Its Z suffix makes it a UTC instant rather than a local date, which is a different thing from what these routes expect; slice a YYYY-MM-DD out of a local-formatted date instead. Building both ends of a window from a single Date avoids a window that silently shifts if the two bounds are computed a moment apart (for example across midnight).

    The example below is the canonical form: it resolves the athlete first and formats the newer bound in me.timezone, then steps the older bound back in whole calendar days rather than in hours, so the window does not move when the range spans a daylight-saving change. Both helpers live in examples/_shared/local-date.ts and are covered by unit tests.

    const client = new IntervalsClient({ apiKey });

    // The API reads `oldest`/`newest` in the athlete's own time zone, so resolve the athlete
    // first and format the bounds in `me.timezone`. Both ends come from one timestamp, so the
    // window cannot shift if the two are computed a moment apart (for example across midnight),
    // and the older bound is stepped back in calendar days rather than in hours.
    const me = await client.athletes.getAthlete();
    const now = new Date(); // one timestamp for both bounds
    const newest = localDateIn(now, me.timezone);
    const oldest = shiftDays(newest, -29); // inclusive bounds: 29 back from today is a 30-day window

    const activities = await client.activities.listActivities({ oldest, newest });
    const events = await client.events.listEvents({ oldest, newest, category: ['WORKOUT'] });
    const wellness = await client.wellness.listWellness({ oldest, newest });

    console.log(`Last 30 days, ${oldest} to ${newest} (inclusive):`);
    console.log(` ${activities.length} activities`);
    console.log(` ${events.length} workout events`);
    console.log(` ${wellness.length} wellness records`);

    The read-only service examples elsewhere in these guides format their windows in the machine's own zone instead, without the extra round trip to resolve the athlete: a read window that is off by a day at one end returns a day more or a day less of data, which is harmless. Resolve the athlete's zone when the exact day matters — writing a wellness record, or placing a calendar event.

    List routes page with oldest/newest and, where the route's options include it, limit (and offset) — see PaginationOptions, which ListActivitiesOptions and other list-options types extend. There are no opaque cursors to carry between calls: to page through a large range, narrow it with oldest/newest/limit and issue another request.

    PaginationOptions also carries resolve, unrelated to paging itself: it resolves nested objects in the response (for example turning a workout's pace targets into actual m/s values) rather than leaving them in their raw, unresolved form.

    Every array-valued option is passed to the SDK as a real array. Never join the values yourself: the SDK decides, per parameter, whether the wire format is one comma-joined value or a repeated key, and a string you joined by hand would be joined or escaped a second time.

    A few named options are joined into a single value before the request is sent, because the route expects one value rather than a repeated key:

    Option Method Sent as
    fields ActivityService.listActivities fields=name,type
    ids ActivityService.downloadFitFiles ids=a,b
    types ActivityService.getStreams types=watts,heartrate
    category EventService.listEvents, EventService.deleteEventsRange category=WORKOUT,NOTE
    activity ids PerformanceService.getActivityPowerCurves, PerformanceService.getActivityHRCurves id=a,b

    ActivityService.getActivities also comma-joins, but into the URL path rather than the query string — see Ids in paths below.

    The comma join for ids on POST /athlete/{id}/download-fit-files is confirmed against the live API (see the Files guide and AUDIT.md): a probe found identical results for ?ids=a,b and ?ids=a&ids=b.

    Any array option not in the table above reaches axios un-joined and is serialized as repeated query keys — tags=race&tags=long — not axios's default bracket/index notation (tags[0]=race). The SDK configures this explicitly (paramsSerializer: { indexes: null } in the HTTP client) because the API is a Spring backend that binds repeated keys to a list parameter but does not understand indexed or bracketed ones. tags on AthleteService.getSummary and types/fatigue on AnalyticsService.getCurves are sent this way.

    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 weekAgo = new Date(now);
    weekAgo.setDate(weekAgo.getDate() - 6);
    const newest = localDate(now);
    const oldest = localDate(weekAgo);

    // `tags` is sent as repeated `tags=` query keys, not a single comma-joined value.
    const summary = await client.athletes.getSummary({ start: oldest, end: newest, tags: ['race', 'long'] });
    console.log(`${summary.length} summary row(s) for tags=race,long`);

    const activities = await client.activities.listActivities({ oldest, newest });
    if (activities.length < 2) {
    console.log('Fewer than two activities in the last 7 days; skipping getActivities/getCurves');
    } else {
    const ids = [activities[0]!.id!, activities[1]!.id!];
    // `ids` is comma-joined by the SDK into the URL path here, not sent as a query parameter.
    const fetched = await client.activities.getActivities(ids);
    console.log(`getActivities(${JSON.stringify(ids)}) returned ${fetched.length} activities`);

    const id = ids[0]!;
    const curves = await client.analytics.getCurves(id, { types: ['watts'], fatigue: ['normal'] });
    console.log(`${curves.length} watts curve(s) for activity ${id}`);
    }

    ActivityService.getActivities takes a list of ids and joins them into the URL path with commas after URL-encoding each one individually (ids.map(encodeURIComponent).join(',')), rather than sending them as a query parameter.

    Encoding is not applied SDK-wide. These are the methods that pass a caller-supplied path segment through encodeURIComponent, so that a #, ?, / or % inside the value cannot be misread as a path separator or the start of a query string:

    Every other method interpolates ids into the path as they are given, so encode a value yourself if it can contain a character that is not URL-safe.