Intervals.icu API Integration Cookbook

This tells you everything you need to know to get completed activities into Intervals.icu, to download planned workouts and to read and update wellness data. Example calls use curl so you can easily try them out.

Sources:

Authorization

Apps need to use an access_token obtained via OAuth. If you are just working with your own data call the API using your personal API key (available towards the bottom of the settings page) and basic authentication.

Uploading completed activities

This is done by POSTing multipart/form-data. Example using an access_token from OAuth (ACTIVITY:WRITE scope is required):

$ curl -F file=@i55610271.fit \
    'https://intervals.icu/api/v1/athlete/0/activities?name=Foo&description=Bar' \
    -H 'Authorization: Bearer d842c1fc25f241e5ae440d09756448a9'

Same example using a personal API key:

$ curl -F file=@i55610271.fit \
    'https://intervals.icu/api/v1/athlete/0/activities?name=Foo&description=Bar' \
    -u API_KEY:6dzmsans11bsyifjxchr87oum

The name and description URL parameters are optional. The file can be fit, tcx or gpx or a zip or gz of the same. There is also an optional external_id parameter which you can set to your own activity ID. You can configure an activity URL template for your application and Intervals.icu will use this and the external_id to link back to your application from the activity pages.

Note that the athlete id in the path is ‘0’. This indicates that the athlete ID that the access_token or API key belongs to should be used.

Downloading planned workouts

You can download planned workouts from an athlete’s calendar in zwo (Zwift), fit, mrc and erg format (CALENDAR:READ scope required):

$ curl 'https://intervals.icu/api/v1/athlete/0/events?category=WORKOUT&ext=zwo&resolve=true' \
    -H 'Authorization: Bearer d842c1fc25f241e5ae440d09756448a9'

The optional category parameter specifies what sort of events to return (planned workouts in this case). The ext parameter the format (leave out if you don’t need the file). The resolve=true parameter converts power (and heart rate etc.) from units like % of FTP into watts. The default date range is a week from today (use oldest and newest parameters in ISO-8601 format to change).

Example abridged response:

[
  {
    "id": 46696127,
    "start_date_local": "2024-11-21T07:00:00",
    "type": "Ride",
    "category": "WORKOUT",
    "name": "Big Gear Grinds",
    "description": "...",
    ...
    "workout_doc": {
      "description": "A great session for building cycling specific strength both in and out of the saddle.",
      "target": "POWER",
      "steps": [
        {
          "text": "Welcome to the ...",
          "duration": 180,
          "warmup": true,
          "ramp": true,
          "power": { "start": 35.0,  "end": 55.0, "units": "%ftp" },
          "cadence": { "value": 85.0,  "units": "rpm" },
          "_power": { "value": 126.0,  "start": 98.0, "end": 154.0 }
        },
      ],
      ...
    },
    ...
    "workout_filename": "Big_Gear_Grinds.zwo",
    "workout_file_base64": "PD94bWwgdmVy..."
  }
]

The workout_file_base64 field contains the file in the specified format (zwo in this case) encoded as base 64 text.

The workout_doc field contains the workout in native Intervals.icu format. If you don’t already have a parser for one of the other formats then this might be easier to work with.

See this guide for instructions on how to upload planned workouts.

Updating wellness data

Push weight, resting HR, HRV, steps etc. using the bulk wellness upload endpoint (WELLNESS:WRITE scope required):

curl -X PUT 'https://intervals.icu/api/v1/athlete/0/wellness-bulk' \
    -H 'Authorization: Bearer d842c1fc25f241e5ae440d09756448a9' \
    -H 'Content-Type: application/json' \
    -d '[{"id":"2024-11-20","weight": 69.1},{"id":"2024-11-19","weight": 69.3}]'

Each object in the array must have an id (the ISO-8601 local date of the record) and whatever other fields you want to update. See the full docs for a list of possible fields. All units are metric.

Downloading wellness data

Call the list wellness endpoint (WELLNESS:READ scope required):

curl 'https://intervals.icu/api/v1/athlete/0/wellness?oldest=2024-11-18&newest=2024-11-20' \
    -H 'Authorization: Bearer d842c1fc25f241e5ae440d09756448a9'

Downloading completed activities

Call the list activities endpoint (ACTIVITY:READ scope is required):

curl 'https://intervals.icu/api/v1/athlete/0/activities?oldest=2024-11-19&newest=2024-11-20' \
    -H 'Authorization: Bearer d842c1fc25f241e5ae440d09756448a9'

This returns summary data for each activity. Example abridged response:

[{
  "id": "i55751783",
  "start_date_local": "2024-11-20T07:35:18",
  "type": "Ride",
  ...
  "file_type": "fit",
  ...

To download the original activity file (fit, gpx or tcx) gzip compressed:

curl 'https://intervals.icu/api/v1/activity/i55751783/file' \
    -H 'Authorization: Bearer d842c1fc25f241e5ae440d09756448a9' \
    > activity.fit.gz

To download the Intervals.icu generated fit file (with edits to activity power etc. and laps matching the intervals):

curl 'https://intervals.icu/api/v1/activity/i55751783/fit-file' \
    -H 'Authorization: Bearer d842c1fc25f241e5ae440d09756448a9' \
    > activity.fit.gz

Note that this is always a fit file even if the original file was a gpx or tcx.

Webhooks

Configure webhooks using the management page for your app. Look for your app in /settings and click “Manage App”.

Note that activity webhooks are not delivered for Strava activities.

The CALENDAR_EVENT_UPDATED and CALENDAR_EVENT_DELETED webhooks are legacy. Please use the CALENDAR_UPDATED webhook instead.

The webhook POST payload includes a secret that you can use to verify that the webhook came from Intervals.icu.

{
  "secret": "ooKeodacie8I",
  "events": [
    {
      "athlete_id": "...",
      "type": "ACTIVITY_UPLOADED",
      "timestamp": "2024-12-06T06:40:47.011+00:00",
      "activity": {...}
    }
  ]
}

Some webhooks (e.g. ACTIVITY_ANALYZED) are sent after a 60s delay so multiple events for the same activity can be consolidated into one webhook.

You need to store the Intervals.icu athlete_id obtained via OAuth flow so you can map the webhook back to the athlete in your system.

CALENDAR_UPDATED webhooks include oauth_client_id (that created the event if any) and external_id (ID supplied when the event was created, if any). You can use this to only look at events created by your app (oauth_client_id matches your client ID) and quickly update them using the external_id.

{
  "secret": "...",
  "events": [
    {
      "athlete_id": "2049151",
      "type": "CALENDAR_UPDATED",
      "timestamp": "2024-12-18T10:22:16.427+00:00",
      "events": [
        {
          "id": 48566307,
          "start_date_local": "2024-12-20T00:00:00",
          "icu_training_load": 118,
          "type": "Ride",
          ...
        },
        {
          "id": 48566308,
          "start_date_local": "2024-12-21T00:00:00",
          "icu_training_load": 82,
          ...
        }
      ],
      "deleted_events": []
    }
  ]
}
21 Likes

Hi David.

Can textevents be made in intervals.icu workouts? I see that repeated intervals send textevents 1/5, 2/5 etc. Can instructions be saved as a textevent? Or do I need to export zwo, make the edit and upload back to intervals.icu?

Thanks

You can create textevent’s in the workout builder:

- Toddle along 30m Z2

ZWO:

<SteadyState show_avg="1" PowerHigh="0.75" PowerLow="0.56" Duration="1800">
    <textevent message="Toddle along" duration="10"/>
</SteadyState>
1 Like

@david I am trying to fix old data for avg_sleep_hr in the wellness data pre 07/02/25 (see why here). I have tried to use both wellness-bulk and wellness endpoints but what is happening is that I can see the data being modified in the UI for a moment, and a moment later it goes back to what it was before, like the system was reverting any change made via the API to what it gets from Oura anyway.

Is this expected? I read here that

Intervals.icu goes back about a week for Oura

But it seems, it only works if you modify that via the UI?

Can you please help me to understand?

Ps. if you wanted to fix you sync algorithm so that it does that by itself I am all happy :smiley:

I think you need to temporarily disable Oura sync , modify, then flag the wellness data as ´blocked´ and enable Oura sync again.
Problem is that I don´t know if you can set ´blocked´ flag in bulk…

you can update a date for testing (note I added the lock = true)

curl -X ‘PUT’ ‘https://intervals.icu/api/v1/athlete/XXXX/wellness/2025-02-14’ -H ‘accept: /’ -H ‘Authorization: Basic XXXX’ -H ‘Content-Type: application/json’ -d ‘{“vo2max”: 50,“steps”: 123,“locked”: true}’

{“steps”:123,“respiration”:null,“locked”:true,“Steps”:444}

After a few min or such, you can do a

curl -X ‘GET’ ‘https://intervals.icu/api/v1/athlete/xxxx/wellness/2025-02-13’ -H ‘accept: /’ -H ‘Authorization: Basic xxxx’

and check if anything has changed. Unfotunately it won’t tell you “what” cause the change tho…

2 Likes

That 'locked': True seems to have done the trick :wink: thanks a lot!

2 Likes

@david any chance that you could enable unlocking (i.e. setting "locked": false) via API for locked entries?

So one could unlock via API, change the wellness data, and then lock again?

(I had some small bugs in my script and now all the wellness data is locked :smiley: )