AI Cycling
014 Build Your Own Training Tools 1,803 words · 8 min

intervals.icu API Versus The Strava API: Which One Should You Build On?

If you are writing anything that reads your own ride files and tells you something useful about them, the intervals icu api is the backend you want. Strava is the front door. It is where the ride lands, where your mates leave kudos, and where every head unit and app on earth already pushes data without you writing a single line of integration code. But as a source of numbers for an analysis tool, it is thin, slow, and legally awkward for exactly the kind of AI tooling this site is about.

That is the argument. Here is the evidence.

The rate limit maths, done properly

Strava publishes hard numbers and enforces them per application, not per athlete. At the time of writing the default allowance is 200 requests every 15 minutes and 2,000 per day overall, with a tighter read-specific budget of 100 per 15 minutes and 1,000 per day. You can apply for an increase. Most people building a personal tool never bother, and never get one.

Now backfill three years of training. Say you are a reasonably keen rider with 1,200 activities since autumn 2022.

TaskStravaintervals.icu
Activity summaries6 requests (200/page)1 request (activities.csv, date range)
Power/HR streams for all 1,2001,200 requests1,200 requests
Daily wellness, 3 yearsnot available1 request (1,096 days)
Best-effort power curverebuild from streams1 request
Wall clock, cold start~2 days (daily read cap)minutes

The streams row is the same on both, which is the honest part. What kills you on Strava is that 1,000-read daily ceiling: 1,206 calls does not fit in one day, so your first sync spans two calendar days and your user watches a progress bar overnight. Then they connect a second athlete and you are queueing.

intervals.icu does not publish per-minute numbers. It throttles, it will return 429 if you hammer it, and the forum guidance from David Tinker (who runs the whole thing) is to cache aggressively and use the bulk endpoints rather than looping. In practice a sensible client doing a few requests per second with backoff is fine. The difference that matters is structural: intervals gives you bulk and CSV variants of nearly everything, so a job that costs Strava 1,206 round trips costs intervals somewhere between 3 and 1,203 depending on whether you actually need raw streams at all. Often you do not, which brings us to the next point.

Streams: what you actually get back

Strava’s stream endpoint accepts a fixed set of keys and returns processed, resampled data:

GET /api/v3/activities/{id}/streams
    ?keys=time,watts,heartrate,cadence,velocity_smooth,altitude
    &key_by_type=true

You get time, distance, latlng, altitude, velocity_smooth, heartrate, cadence, watts, temp, moving, grade_smooth. That is the list. No left/right balance, no torque effectiveness, no pedal smoothness, no respiration rate, no core temperature, no SmO2 or tHb if you run a Moxy, no gear ratios from a Di2 or AXS log. Your Assioma Duos record balance every second and Strava throws it away before you ever see it.

intervals.icu keeps whatever your head unit wrote. The equivalent call looks like this, and the CSV variant is a genuinely nice touch when you are prototyping:

GET /api/v1/activity/{id}/streams.csv?types=watts,heartrate,cadence,torque,respiration
Authorization: Basic base64("API_KEY:your_key_here")

time,watts,heartrate,cadence,torque,respiration
0,0,94,0,0.0,14.2
1,112,95,71,15.1,14.4
2,241,97,88,26.2,15.1
3,268,101,89,28.8,15.9

Auth is HTTP Basic with the literal username API_KEY and your key as the password, grabbed from Settings in the web app. No OAuth dance for your own data, which means a working script in about four minutes rather than an afternoon of redirect URI debugging.

The original file is sitting right there

Here is the detail that ends the argument for anyone doing serious analysis. intervals.icu will hand you back the file your Garmin or Wahoo actually recorded:

GET /api/v1/activity/{id}/file

That is the raw FIT, GPX or TCX, byte for byte. Pipe it through fitdecode or fitparse and you have every developer field, every message type, every proprietary extension your power meter writes. Strava has no such endpoint. Once your ride is inside Strava, the original is gone as far as the API is concerned, and you are stuck with Strava’s interpretation of it.

For AI tooling this is not a nice-to-have. If you are building something that reasons about pacing quality on a 25-mile TT, the difference between real 1Hz power and Strava’s smoothed stream shows up immediately in variability index and decoupling calculations.

Wellness: Strava has nothing, and it is not close

The /athlete object on Strava gives you a weight. That is the entirety of Strava’s physiological picture of you.

intervals.icu has a first-class wellness endpoint with a date range:

GET /api/v1/athlete/{id}/wellness?oldest=2026-09-01&newest=2026-09-30

[
  {
    "id": "2026-09-29",
    "ctl": 78.4, "atl": 91.2, "rampRate": 4.1,
    "restingHR": 46, "hrv": 82, "hrvSDNN": 61,
    "sleepSecs": 25200, "sleepScore": 74,
    "weight": 71.2, "fatigue": 3, "soreness": 2,
    "readiness": 68, "spO2": 97, "vo2max": 58.1
  }
]

Fields cover resting HR, HRV (both rMSSD and SDNN), sleep duration and score, weight, subjective fatigue, soreness, stress, mood, motivation, menstrual phase, blood glucose, lactate, steps, SpO2, blood pressure and more, plus your CTL/ATL/ramp rate computed daily. It is writable too, so your tool can PUT back a computed readiness score or a note and have it show up on the athlete’s calendar alongside everything else.

Try building an AI coach that answers “should I do the 3x12 at threshold today or push it to Thursday?” without HRV, sleep and a subjective fatigue score. You cannot, and Strava will never give you them.

Metrics you would otherwise have to rebuild

Strava’s activity object gives you average_watts, weighted_average_watts, kilojoules, device_watts and suffer_score. The device_watts boolean is genuinely useful because it tells you whether the power is real or estimated from a GPS trace, and any tool that skips that check will happily feed your LLM garbage from a group ride you did on a borrowed bike.

intervals.icu ships a much larger set on every activity, prefixed so they are easy to spot: icu_training_load, icu_ctl, icu_atl, icu_ftp, icu_weighted_avg_watts, icu_efficiency_factor, icu_variability_index, icu_intensity, decoupling, polarization_index, session_rpe, feel. There is also a power curve endpoint that returns best efforts over whatever periods you ask for, and an intervals endpoint that returns your laps or auto-detected efforts with average power, normalised power, W’ spent and decoupling per interval.

Consider what that saves. Rebuilding a 90-day power curve from Strava means pulling 90 days of watt streams (call it 40 requests, then a rolling-max pass over roughly two million samples). On intervals it is one GET. If you are feeding an LLM, one GET also means one small JSON blob in your prompt instead of a token budget catastrophe.

The terms problem nobody mentions until they get an email

Strava’s API Agreement was tightened in November 2024 and it matters enormously for the readers of this site. It explicitly prohibits using Strava data to train AI or machine learning models, and it sharply limits displaying one athlete’s data to anyone else. The language is broad enough that if you are piping ride streams into a model, you should read it properly rather than assume “analysis for the athlete themselves” is obviously fine.

intervals.icu’s position is simpler: the athlete generates an API key for their own account and gives it to your tool. It is their data, their key, their call. For third-party apps serving other people there is an OAuth flow, arranged by emailing the developer, and the terms are not hostile to the thing you are trying to build.

So why keep Strava at all

Because it is the best ingest layer in the sport and nothing else is close. Every head unit, Zwift, Wahoo SYSTM, TrainerRoad, Peloton, Hammerhead, Coros, Suunto, and half the apps on your phone push to Strava by default. Your users already have an account. They already recognise the OAuth consent screen and will click through it without emailing you to ask if it is a scam.

More importantly, Strava has webhooks and intervals.icu does not have an equivalent push mechanism. Register a push subscription and Strava POSTs you an event within seconds of an upload:

{
  "object_type": "activity",
  "object_id": 15123456789,
  "aspect_type": "create",
  "owner_id": 1234567,
  "event_time": 1790000000
}

That event is your trigger. It costs you nothing, it arrives near-instantly, and it replaces the polling loop that would otherwise burn your rate limit doing nothing. There is one subscription per application, so this is a single piece of infrastructure to run.

The architecture that actually works

Wire it up like this:

Garmin / Wahoo / Zwift
        ↓ (auto-upload)
      Strava  ──webhook──▶  your endpoint
        ↓ (auto-sync)              │
   intervals.icu  ◀────────────────┘
        ↓ (pull: streams, wellness, power curves, FIT)
   your analysis / LLM layer

Strava tells you a ride happened and who it belongs to. Wait 60 to 120 seconds for intervals.icu to sync it across (it pulls from Strava automatically, and also directly from Garmin Connect, Wahoo, Polar, Suunto, Dropbox and others), then match on the Strava activity ID stored in the intervals activity record and pull everything you actually need from intervals. You use maybe five Strava read requests a week per athlete. You never hit the cap.

If you want the full walkthrough of standing this up, including the webhook verification handshake and a sensible caching layer, the Build Your Own Training Tools pillar covers the scaffolding end to end.

Where intervals.icu will bite you

Be clear-eyed about the trade. intervals.icu is essentially one developer with a Swagger page at intervals.icu/api-docs and a forum. There is no SLA, no status page you would show a customer, no published deprecation policy, and no rate limit documented in a form you could put in a runbook. Endpoints have changed shape before. If your product’s revenue depends on it, that is a real risk to price in, and you should cache everything locally rather than treating the API as your database.

Strava, for all its restrictions, has a company behind it. The docs are stable, the client libraries are mature (stravalib in Python does the token refresh dance properly), and the behaviour you build against in September will still be there in March.

Start here

Generate an intervals.icu API key, find your athlete ID (it is the i number in the web app URL), and run one request:

curl -u "API_KEY:$INTERVALS_KEY" \
  "https://intervals.icu/api/v1/athlete/i123456/activities.csv?oldest=2026-07-01&newest=2026-09-30"

Ninety days of rides, every icu_ field included, in a single CSV you can open in pandas before your coffee goes cold. Do the same thing against Strava and you will spend the morning on OAuth refresh tokens and still not have a decoupling figure. That gap is the whole reason to pick a side.