API access to Intervals.icu

What error? Any error log?
Looks ok, if the first one works.

Update for sensor.icu_bike_distance_ytd fails
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/template/__init__.py", line 2065, in forgiving_float_filter
    return float(value)
TypeError: float() argument must be a string or a real number, not 'NoneType'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 963, in async_update_ha_state
    await self.async_device_update()
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1314, in async_device_update
    await self.async_update()
  File "/usr/src/homeassistant/homeassistant/components/rest/entity.py", line 64, in async_update
    self._update_from_rest_data()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/src/homeassistant/homeassistant/components/rest/sensor.py", line 178, in _update_from_rest_data
    value = self._value_template.async_render_as_value_template(
        self.entity_id, variables, None
    )
  File "/usr/src/homeassistant/homeassistant/helpers/trigger_template_entity.py", line 134, in async_render_as_value_template
    render_result = render_with_context(
                    ~~~~~~~~~~~~~~~~~~~^
        self.template, compiled, **variables
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ).strip()
    ^
  File "/usr/src/homeassistant/homeassistant/helpers/template/context.py", line 45, in render_with_context
    return template.render(**kwargs)
           ~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/jinja2/environment.py", line 1295, in render
    self.environment.handle_exception()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/local/lib/python3.13/site-packages/jinja2/environment.py", line 942, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<template>", line 3, in top-level template code
  File "/usr/local/lib/python3.13/site-packages/jinja2/async_utils.py", line 48, in wrapper
    return normal_func(*args, **kwargs)
  File "/usr/local/lib/python3.13/site-packages/jinja2/filters.py", line 1336, in sync_do_sum
    return sum(iterable, start)  # type: ignore[no-any-return, call-overload]
  File "/usr/local/lib/python3.13/site-packages/jinja2/filters.py", line 1503, in sync_do_map
    yield func(item)
          ~~~~^^^^^^
  File "/usr/local/lib/python3.13/site-packages/jinja2/filters.py", line 1745, in func
    return context.environment.call_filter(
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        name, item, args, kwargs, context=context
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/usr/local/lib/python3.13/site-packages/jinja2/environment.py", line 569, in call_filter
    return self._filter_test_common(
           ~~~~~~~~~~~~~~~~~~~~~~~~^
        name, value, args, kwargs, context, eval_ctx, True
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/usr/local/lib/python3.13/site-packages/jinja2/environment.py", line 550, in _filter_test_common
    return func(*args, **kwargs)
  File "/usr/src/homeassistant/homeassistant/helpers/template/__init__.py", line 2068, in forgiving_float_filter
    raise_no_default("float", value)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/template/helpers.py", line 13, in raise_no_default
    raise ValueError(
    ...<2 lines>...
    )
ValueError: Template error: float got invalid input 'None' when rendering template '{%- set Ride_TYPES = ['Ride','VirtualRide','GravelRide'] -%} {%- set acts = value_json if value_json is iterable else [] -%} {{ (acts | selectattr('type','in',Ride_TYPES)
         | selectattr('distance','defined')
         | map(attribute='distance') | map('float') | sum / 1000) | round(1) }}' but no default was specified

Seems there are activities with the field distance, but no value (or ‘null’).
Adding

selectattr('distance','!=', None) 

so maybe try something like this:

{%- set Ride_TYPES = ['Ride','VirtualRide','GravelRide'] -%}
{%- set acts = value_json if value_json is iterable else [] -%}
{{ (acts
    | selectattr('type','in',Ride_TYPES)
    | selectattr('distance','defined')
    | selectattr('distance','!=', None) 
    | map(attribute='distance')
    | map('float')
    | sum / 1000) | round(1) }}

Thank you very much. This is the solution.

1 Like

Maybe someone can help me with a similar problem - ChatGPT is too dumb for this. Sensors are reporting 0.

=========================

:bicycle: Bike Distance – Monat & Woche (gleiche Logik)

=========================

  - platform: rest
    name: ICU Bike Distance This Month
    unique_id: icu_bike_distance_month
    resource_template: >-
      https://intervals.icu/api/v1/athlete/1450737/activities?oldest={{ now().strftime('%Y-%m-01') }}&newest={{ (now().date() + timedelta(days=1)) }}&limit=5000
    method: GET
    authentication: basic
    username: API_KEY
    password: !secret intervals_api_key
    headers: { Accept: application/json }
    value_template: >-
      {%- set Ride_TYPES = ['Ride','VirtualRide','GravelRide'] -%}
      {%- set acts = value_json if value_json is iterable else [] -%}
      {{ (acts
        | selectattr('type','in',Ride_TYPES)
        | selectattr('distance','defined')
        | selectattr('distance','!=', None)
        | map(attribute='distance') | map('float') | sum / 1000) | round(1) }}
    unit_of_measurement: "km"
    scan_interval: 3600
  
  - platform: rest
    name: ICU Bike Distance This Week
    unique_id: icu_bike_distance_week
    resource_template: >-
      https://intervals.icu/api/v1/athlete/1450737/activities?oldest={{ (now().date() - timedelta(days=now().weekday())) }}&newest={{ (now().date() + timedelta(days=1)) }}&limit=5000
    method: GET
    authentication: basic
    username: API_KEY
    password: !secret intervals_api_key
    headers: { Accept: application/json }
    value_template: >-
      {%- set Ride_TYPES = ['Ride','VirtualRide','GravelRide'] -%}
      {%- set acts = value_json if value_json is iterable else [] -%}
      {{ (acts
        | selectattr('type','in',Ride_TYPES)
        | selectattr('distance','defined')
        | selectattr('distance','!=', None)
        | map(attribute='distance') | map('float') | sum / 1000) | round(1) }}
    unit_of_measurement: "km"
    scan_interval: 3600

Hard to help without error logs :wink:

No error message Result = 0

Hm, I guess that’s wrong?
I don’t know the difference (and I am too lazy to look this up), but there is also a field icu_distance. Try this instead of distance.

And I am sure you don’t need the newest and limit parameter. You can remove those parts from the url.

I cant get ChatGPT to connect to intervals.icu API, no matter what solution I use from the other thread. All tested, selfmade or from others custom GPT keeps getting 403 Access denied, even though the credintials etc. works.

Can I get aproved to use Oauth, because that works, but can’t find one that can push to the calendar, so will create a new Coach GPT with OAuth

Without posting your url / API endpoint, you can’t get any help here. It could be that you even try to access a different user account, that would be a 403.
So what endpoint are you trying to Access?

Best to Post curl without your api key

It works with Curl and Phyton, just can’t ChatGPT to connect via Basic Auth.

1 Like

@david - I am just playing around with the API URL https://intervals.icu/api/v1/athlete/{athelete_id} and it’s returning some long since retired bikes for my profile.

Not a huge deal as I dont really need that information, but I thought I’d flag it

1 Like

Did you succeed? I’m interested for the same, actually many people would I think :wink: If anyone could do some tutorials and publish on YT this would be great. Or make some Wiki pages, or even better publish README on Git.
Right now I have a training plan recorded in text and tables in a Google Sheet, following prompts and conversation with ChatGPT. I have built the plan in Stryd, and would like to also test parallel approach with Intervals. My plan is to prepare a 80k trail run in March 2026.

Hello,

I’m seeking assistance with the Intervals API.

Context
Some users would like to sync or import gym workouts from the Hevy app (refer to the features requests) to Intervals ICU.
Hevy offers a developer API for Pro users, while Intervals ICU has a free API.

Currently, it’s quite straightforward to import a Hevy workout (using the Hevy API, which is not detailed here) and insert it into Intervals ICU using a POST request to api/v1/athlete/{id}/activities/manual.

Problem
There’s no limitation preventing the insertion of the same activity twice.

The POST request to /api/v1/athlete/{id}/events/bulk has a query parameter upsert that specifies whether to update events with matching external_id and created by the same OAuth application instead of creating new ones.


Question 1
I noticed that the external_id is also present in the POST request to api/v1/athlete/{id}/activities/manual.
Therefore, I’m curious to know if there’s currently a way to achieve the same behavior as the upsert=true query parameter in the /api/v1/athlete/{id}/events/bulk request.

If not, how would you handle this to prevent the same activity from being inserted twice from the Hevy app?

Solution I propose (not very elegant)

  1. Get heavy workouts.
  2. Get interval ICU activities (parameters: date range matching the heavy workout date range).
    → Filter the heavy workouts based on the existing external IDs from the interval activities.
  3. Post the remaining heavy workouts to the interval ICU.

→ It would be nice to avoid step 2 as it increases the complexity.


Question 2
Is there a way to create multiple activities for events (likeevents/bulk)?
It’s not a big deal, but it might speed up the synchronization with other apps as well.


Thank you for reading this and for your help

Also I wish to contribute to this project. I noticed that David was not initially planning to make it open-source, but I still added my vote in the hope that it might influence him to reconsider.

1 Like

I will have a look at this problem and figure something out. Normal activities are de-duped using a hash of the file. Certainly creating activities is something that needs to be very easy.

By file you mean the whole data of an activity or only the .fit file?

Just to precise my case,

There’s no limitation preventing the insertion of the same MANUAL activity twice.

Manual activities like gym workout have no files.

Thank you for your work!

Work on /v1/athlete/{id}/activities/manual/bulk is in progress. This will do an upsert using the oauth client and external_id as you suggest.

1 Like

Crazy reactivity! Thank you so much.

Note:
I’ve dropped a Hevy Sync with ICU integration here. I’ve replaced it with an AWS Lambda function for automatic synchronization since Hevy provides a webhook. Perhaps we could contact them directly to establish a partnership that would simplify synchronization. Currently, it requires a Pro account on Hevy.

1 Like

The new endpoint is live now:

Create multiple manual activities with upsert on external_id

POST /api/v1/athlete/{id}/activities/manual/bulk

Existing activities with matching external_id, created by the same OAuth application are updated. Activities created/updated are returned. Activities with no external_id are always created.

https://intervals.icu/api-docs.html#post-/api/v1/athlete/-id-/activities/manual/bulk

1 Like