Announcing CIMD support for MCP Client registration
Learn more

MCP vs API for Integrating Strava to AI Agents

Strava gives an agent two surfaces: an official, read-only MCP server for subscribers, and the Strava API v3 with per-athlete OAuth and webhooks. On Strava the engineering comparison comes second, because the API Policy that took effect on June 1, 2026 decides which path a model may sit on at all. This guide covers what each path can do, what the policy allows, the rotating credential every athlete grant carries, and how to run the parts a multi-athlete product can build today.

TL;DR

  • Strava ships an official, hosted Model Context Protocol (MCP) server at https://mcp.strava.com/mcp, launched with Claude on June 1, 2026 and with Google Gemini (US only) in October 2026. It is read-only, OAuth-based, and included only with a Strava subscription. Strava's API Policy authorizes it only for "personal use of their own Strava data" and says it "may not be used to enable, any commercial or third-party access."
  • The Strava API v3 covers athletes, activities, streams, laps, zones, segments, segment efforts, routes, clubs, gear, and uploads. It uses OAuth 2.0 with seven scopes, six-hour access tokens, and refresh tokens that are invalidated the moment a new one is issued. Rate limits are per application: 200 requests per 15 minutes and 2,000 per day by default, and new apps can serve exactly one athlete until upgraded.
  • The API Policy effective June 1, 2026 is the real decision point. Section 5.3 bars using the API or Strava Data "in connection with the development, training, evaluation, or operation of any AI Application," and lists "ingestion into a context window or working memory" and retrieval-augmented generation among the covered activities. Section 5.16(b) bars operating "any MCP Server, agent-mediated interface, or analogous mechanism" that exposes the API. The only carve-out is the Strava MCP.
  • For a multi-athlete agent product, neither path is open under the published terms: the Strava MCP is personal-use only, and the API excludes AI use. What the API still supports is the non-AI half of a product: OAuth per athlete, webhook-driven sync, display back to the same athlete, and deauthorization handling. Putting Strava Data in front of a model in production needs an agreement with Strava first.
  • Scalekit's strava connector exposes 28 tools over the Strava API (24 read, 4 write, 0 destructive) with OAuth 2.0 through your own Strava app. Scalekit stores and rotates each athlete's tokens in the token vault, which is the hard part of Strava auth whichever way your terms conversation goes.

The decision your Strava agent forces first

Your product is a coaching platform. Forty coaches each work with about 25 athletes, and every athlete records on Strava. The pitch for the agent is obvious: read each athlete's last week of activities, compare heart rate zones against the plan, flag the athlete whose easy runs have crept into zone 4, and draft the coach's Monday notes. You go looking for a Strava MCP server and find one, official and hosted, announced with Claude in June. You also find the Strava API, which you have probably used before. Then you read the June 2026 API Policy and discover that the question is not only which path is better engineered. It is which path you are allowed to put a model on. Here's how to pick.

What Strava MCP and Strava API actually are

Strava MCP

The Strava MCP connector is a remote MCP server operated by Strava at https://mcp.strava.com/mcp. Strava's help article lists Claude support since June 2026 (claude.ai, the desktop app, and Claude Code with claude mcp add --transport http strava-mcp https://mcp.strava.com/mcp) and Google Gemini since October 2026, available only in the US at launch. Athletes authorize it with OAuth by signing in to Strava, and revoke it under Settings > My Apps on Strava.com.

It is read-only: per Strava, the assistant "cannot upload, edit, or delete activities." Strava describes the data rather than publishing tool names: activity history, fitness trends, cross-sport insights, gear, best efforts, activity weather, and streaks, with launch coverage from Marathon Handbook adding per-second heart rate and pace, GPS routes, and cycling power. Requests are rate limited per minute and per day; Strava has not published the numbers. It is included with the Strava subscription, the Strava + Runna bundle, and the Family, Student, and discount plans, and with nothing else.

Community MCP servers also exist. The most-starred, r-huijts/strava-mcp (MIT), says it "exposes 25 tools for Strava API v3" across profile, activities, stats, segments, routes, and clubs, runs locally, and stores OAuth tokens in ~/.config/strava-mcp/config.json from a Strava app the user registers. Since June 1, 2026, Strava's policy treats running a server like this for anyone as a prohibited "MCP Server" under section 5.16(b), and Standard Tier API access now requires a Strava subscription.

Strava API

The Strava API v3 is REST at https://www.strava.com/api/v3. It covers the authenticated athlete, their stats and zones, activities with laps, zones, comments, kudos, and streams (time, distance, latlng, altitude, heart rate, cadence, watts, temperature), segments and segment efforts, routes with GPX and TCX export, clubs, gear, and file uploads. Strava's June 2026 developer update moves the base URL to the api-v3.strava.com host and requires tokens in request headers by June 1, 2027.

Auth is OAuth 2.0 only: send the athlete to https://www.strava.com/oauth/authorize with response_type=code and a comma-delimited scope, then exchange the code at POST https://www.strava.com/oauth/token. There are no API keys and no service accounts. Every call is made as one athlete.

Read Strava's API Policy before you pick a path

Most articles in this series compare two engineering surfaces. Strava's terms decide this one first, so they come before the comparison. Three dates matter.

On November 15, 2024, Strava updated its API agreement to limit display of a user's data to that user and stated that its "previous terms already disallowed the use of Strava user data in model training and development." That version restricted training. It did not address inference.

On June 1, 2026, a new API Agreement and API Policy took effect. The policy is the document an agent builder has to read in full. Its relevant sections, quoted directly:

  • Section 5.3, AI use. "You may not use the Strava API Materials or Strava Data, directly or indirectly, in connection with the development, training, evaluation, or operation of any AI Application." The covered activities include "grounding, evaluation, benchmarking, embedding generation, retrieval-augmented generation, ingestion into a context window or working memory." Derived, aggregated, and anonymized data are covered too. The section closes: "This prohibition does not extend to use of the Strava MCP."
  • Section 5.16, agent interfaces. You may not "operate any MCP Server, agent-mediated interface, or analogous mechanism that exposes the Strava API Materials," nor "share, transfer, multiplex, or otherwise re-use API Tokens" across servers or end users, nor operate an "abstraction layer, integration-platform-as-a-service, no-code-AI platform."
  • Section 3.5, the Strava MCP. It is "the sole authorized first-party agent-mediated interface." Subscribers "may bring their own AI Application to interact with their own data through the Strava MCP," and it "is not authorized for, and may not be used to enable, any commercial or third-party access."
  • Sections 2.3 and 6.1, display. Data from one user can be displayed only to that user, even when it is public on Strava. Section 6.1 words this as applying "unless your Developer Application has an athlete capacity of 9,999 or less," while the Agreement states the single-user rule without that qualifier; Terra's analysis reads the Agreement as controlling and recommends asking Strava to clarify. A coach viewing an athlete's data sits inside that ambiguity.
  • Section 6.2, caching. "You may not retain Strava Data in your cache for longer than seven (7) days."

On September 1, 2026, the Club Activities, Club Members, and Club Administrators endpoints were deprecated, and Explore Segments became limited to approved Extended Access apps.

Read fairly, Strava's position is coherent. It says developer applications rose 448% in 2026, cites AI scraping and "zero-code AI tools" that hammer the API, and offers its own MCP as the AI path for athletes analyzing their own data. Read precisely, the consequence for this article is blunt. An agent that pulls an athlete's activities from the API into a model's context is the activity section 5.3 names. A server that exposes Strava tools to an agent is the mechanism section 5.16(b) names. The policy contains no "unless agreed in writing" carve-out for either, so a production agent product needs a conversation with Strava at developers@strava.com before it needs an architecture.

This is not legal advice, and terms change. It is the published text as of November 2026.

Comparing them where it matters for agents

What your agent can actually do

Capability
Strava MCP
Strava API v3
Vendor-operated, hosted endpoint
Yes
Yes: Strava-owned REST contract
Who it may serve
Limited: A subscriber's personal use of their own data only (section 3.5)
Limited: Any authorized athlete, but not in connection with any AI Application (section 5.3)
Activity history and detail
Yes: Activity history, best efforts, weather
Yes
Per-second streams (heart rate, power, GPS)
Yes: Per Strava's launch materials
Yes
Heart rate and power zones
Yes: Training analytics included
Limited: Athlete zone boundaries; per-activity zones are a subscriber feature
Segments and efforts
Yes
Limited: Segment efforts need a subscription; segment explore needs Extended Access
Routes and gear
Yes
Yes
Club rosters and club feeds
No: Club metadata only
No: Club Members and Club Activities deprecated September 1, 2026; club details remain
Edit activities, log manual activities
No
Yes
Upload FIT, GPX, or TCX files
No
Limited: Uploads in the API; Scalekit only polls upload status
React to new activities and deauthorizations
No: Answers only when asked
Yes: One webhook subscription per app
Many athletes under one product
No: Each subscriber connects their own AI client
Yes: Authorization code flow per athlete, after athlete capacity review

The MCP column is stronger than a "read-only" label suggests. For a subscriber asking Claude why their threshold pace stalled this spring, it reaches streams, zones, and best efforts with no developer app, no review, and no rate limit math. Strava also says answers come from live data rather than an export.

The API column is wider, and it is the only one with writes, uploads, and webhooks. It is also the column where the tool exists but the use is restricted: subscriber-only endpoints, Extended Access endpoints, and endpoints Strava has removed. The Scalekit tool list reflects that, with strava_explore_segments documented as limited to "apps with Extended Access Tier permission."

What the MCP path makes easier

"How did my long runs this block compare with last year's, and was I drifting above zone 2?" on the raw API means GET /athlete for the athlete ID, paging GET /athlete/activities with after and before epoch timestamps, filtering by sport_type, fetching zones per activity, pulling heart rate streams at one sample per second for each run, and summarizing arrays that run to thousands of points each. Every byte lands in the model's context.

The Strava MCP collapses that into a conversation. The athlete adds a connector, signs in, and asks. Strava owns the schemas, the paging, and the rate limiting. For a single athlete, it is the shortest path from question to answer, and it is the path Strava's terms explicitly bless.

What it does not do is serve anyone but the person who connected it. There is no per-tenant configuration, no way for your backend to call it on an athlete's behalf, and no write access. It is a consumer feature with an MCP transport, not an integration surface.

The auth path each one puts you on

On the MCP path, auth is Strava's own OAuth consent, run by the AI client. The athlete signs in to Strava from Claude or Gemini, approves access, and the client holds the token. You never see it, and section 3.5 means you are not supposed to be in that loop for anyone but yourself.

On the direct API path, you register an app at strava.com/settings/api and run the Authorization Code flow per athlete. Strava's scopes are granular, which is unusual among fitness APIs:

  • read and read_all for public and private segments, routes, and events
  • profile:read_all and profile:write for the full profile, weight, FTP, and segment stars
  • activity:read, activity:read_all, and activity:write for activities, private ones and privacy zone data, and edits and uploads

Athletes can uncheck scopes on the consent screen, so check the scope value Strava returns rather than assuming what you asked for. An athlete who granted activity:read without activity:read_all silently loses Only You activities, and Strava's webhook docs note those apps get a delete event when an activity turns private.

The token lifecycle is where Strava gets strict. Access tokens "expire six hours after they are created." Refreshing with grant_type=refresh_token may return a new refresh token, and "once a new refresh token is returned, the older refresh token is invalidated immediately." Two workers refreshing the same athlete at the same moment is not a cosmetic race: the loser holds a dead refresh token, and the athlete has to reauthorize. Revocation moved too. POST https://www.strava.com/oauth/revoke, authenticated with HTTP Basic using your client ID and secret, is available since June 1, 2026 and becomes the only deauthorization endpoint on June 1, 2027, when oauth/deauthorize retires.

Granular scopes make least privilege expressible, which most connectors in this series cannot claim. What the user can't do, the agent can't do; on Strava you can also make sure the integration asks for less than the user could grant.

Both paths still need per-user isolation

In a multi-tenant B2B agent, MCP gives you a credential per user and the direct API gives you a credential per user. In neither case does the path itself solve storage, rotation, or revocation; those are infrastructure problems regardless of which path you choose.

Strava adds two constraints of its own. Section 5.16(c) forbids re-using "API Tokens or authentication credentials across multiple servers, services, applications, or end users," so the shortcut of one coach's token reading a squad's data is a terms violation as well as a scoping failure. And Strava's athlete model has no organization: there is no team token, no admin grant, no workspace. A coaching platform with 1,000 athletes holds 1,000 independent grants, each with its own scopes, its own six-hour clock, and its own rotating refresh token.

Recommended reading: How to handle token refresh for AI agents

What you own in production

On the MCP path, you own nothing and control nothing. Strava runs the server, sets unpublished per-minute and per-day limits, and decides which data types ship next. That is the right trade for an athlete. For a product, it is not a dependency you can build on, because the policy says you cannot.

On the API path, you own capacity first. New apps start at an athlete capacity of 1, which Strava's rate limit docs call Single Player Mode. A self-service upgrade raises capacity to 10 athletes and limits to 400 requests per 15 minutes and 4,000 per day overall, with 200 and 2,000 for reads. Beyond 10 athletes, Strava reviews the app, and "until approved, you cannot authenticate additional athletes." Standard Tier covers up to 9,999 athletes after review; Extended Access is "generally inclusive of Developer Applications serving 10,000 users or more and approved by Strava." Since June 30, 2026, Standard Tier developers need a Strava subscription; Extended Access developers do not.

The defaults are 200 requests per 15 minutes and 2,000 per day overall, 100 and 1,000 for reads, and they are per application, not per athlete. The 15-minute window resets at 0, 15, 30, and 45 minutes past the hour, and the daily window at midnight UTC. Strava returns 429 Too Many Requests past either, and reports usage in X-RateLimit-Usage and X-ReadRateLimit-Usage as two comma-separated numbers. Polling 1,000 athletes for new activities is not possible at these numbers, which is why Strava's own guidance for apps that ask for more is to use webhooks and throttle backfill.

Webhooks are how a headless integration learns anything. Each app gets one push subscription covering every athlete who authorized it. Four properties shape the handler:

  1. Creation is a handshake. POST /api/v3/push_subscriptions with client_id, client_secret, callback_url, and verify_token triggers a GET to your callback with hub.mode, hub.challenge, and hub.verify_token. You must return 200 within two seconds and echo hub.challenge as JSON.
  2. Events are pointers. A delivery carries object_type (activity or athlete), object_id, aspect_type (create, update, delete), updates, owner_id, subscription_id, and event_time. You fetch the activity with the owner's own token.
  3. Deauthorization arrives as an athlete update. When an athlete revokes access, you receive object_type: athlete with updates containing "authorized": "false". That event is your trigger to stop calling, mark the connected account, and purge cached Strava Data.
  4. There is no signature. Strava documents no signing scheme. Check subscription_id against your own, never trust payload contents beyond IDs, and read the object back with the athlete's grant. Strava retries a failed delivery up to three attempts total, so handlers must be idempotent and respond before doing the work.

Section 6.2's seven-day cache limit belongs in this list too. A coaching history that persists Strava activities indefinitely is a storage design the policy rules out.

When to use MCP, when to use the API

Use the Strava MCP when:

  • An athlete with a Strava subscription wants Claude or Gemini to analyze their own training: pace trends across a marathon block, time in zone by week, or gear mileage before replacing shoes.
  • A coach who is also a Strava subscriber wants an assistant over their own training log in Claude Code or the Claude desktop app.
  • Your product wants to point athletes at an AI analysis option without touching their data: link to Strava's setup instructions and let them connect their own client, which is what section 3.5 authorizes.

Use the Strava API when:

  • Your platform needs per-athlete OAuth for many athletes, with scopes that match the feature, and the integration does not feed Strava Data to a model.
  • A webhook-driven pipeline syncs each new activity into the athlete's own view in your app, handles deletes and privacy changes, and purges data within seven days.
  • The integration writes: strava_update_activity to attach the right gear_id or mark a commute, strava_create_activity to log a coach-prescribed strength session, or an upload of a FIT file from your own device.
  • Your team has reached an agreement with Strava that covers AI use, in which case the API is the only surface that supports a multi-athlete agent at all.

The credential problem that exists on both paths

Both paths hand you a Strava credential per athlete. Neither hands you a vault. A coaching platform with 40 coaches and 1,000 athletes holds 1,000 grants. At a six-hour access token lifetime, keeping them warm means about 4,000 refreshes a day, and every refresh can rotate the refresh token and kill the old one on the spot. Any two processes that refresh the same athlete without coordination produce a dead grant and a support ticket that says "Strava disconnected again." Each grant must be stored encrypted, kept out of logs and LLM context, mapped to exactly one athlete, checked for the scopes actually granted, and revoked through oauth/revoke when the athlete leaves, with their cached data deleted.

The MCP path doesn't reduce the count; it moves the tokens into each athlete's AI client, where your product cannot see them, which is exactly what Strava intends. The tempting shortcut on the API path, one coach's token reused for the whole squad, is both a cross-tenant scoping failure and a section 5.16(c) violation.

Scalekit's strava connector runs the OAuth flow per athlete, stores each token in the token vault, and refreshes it centrally, so the rotation race lives in one place instead of every worker. Your code passes an identifier; credentials never touch the agent runtime or the LLM context. That holds for a non-AI sync pipeline today and for an AI agent if Strava agrees to one.

Recommended reading: Why a token vault matters for AI agent workflows

How to connect a Strava agent with Scalekit

Scalekit's strava connector is an API-path connector built by Scalekit with 28 tools: 24 read-only, 4 write, and none destructive. The write tools are strava_create_activity, strava_update_activity, strava_update_athlete (weight only), and strava_star_segment. It covers the athlete profile, stats, and zones; activities, laps, zones, comments, kudos, and streams; segments, starred segments, segment efforts, and their streams; routes and route streams; clubs; gear; and upload status. The core set for a coaching integration:

Tool
What it does
strava_get_athlete
Returns the authenticated athlete's profile; detailed with profile:read_all
strava_list_activities
Lists the athlete's activities, newest first, filterable by after and before epoch seconds
strava_get_activity
Returns one activity with splits, laps, gear, and segment efforts
strava_get_activity_streams
Returns per-sample streams such as time,heartrate,watts
strava_get_activity_zones
Returns time in each heart rate and power zone; a Strava subscriber feature
strava_get_athlete_zones
Returns the athlete's zone boundaries; needs profile:read_all
strava_update_activity
Renames an activity, sets gear_id, or toggles commute, trainer, and hidden flags
strava_create_activity
Logs a manual activity; cannot attach GPS files

The connector authenticates with OAuth 2.0 through your own Strava app; there is no Scalekit-managed Strava app. The full schemas are on the connector docs page.

One rule governs the rest of this section. The Scalekit mechanics are the same whether a model is involved or not, but Strava's terms are not. The deterministic execute_tool and REST examples below are the shape of a compliant non-AI integration. The agent loop and the virtual MCP server put Strava Data in a model's context and expose Strava tools through an MCP endpoint, which sections 5.3 and 5.16(b) prohibit; build them only once Strava has agreed to that use with you.

Prerequisites and the connection name

In the Scalekit dashboard, go to AgentKit > Connections, create a Strava connection, and copy its redirect URI. On Strava, open strava.com/settings/api, register your application, and set its Authorization Callback Domain to the host of that redirect URI. Enter the app's Client ID and Client Secret on the Scalekit connection, and request only the scopes your feature needs. The name you give the connection is the connection_name your code passes, and the string must match the dashboard exactly. This is the most common integration error. Copy SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET from Developers > API Credentials into .env.

Remember Strava's capacity rules while testing: until you self-upgrade, your app can authorize only you, and past 10 athletes it needs Strava's review.

pip install scalekit-sdk-python python-dotenv langchain-openai

Authorize each user once

get_or_create_connected_account returns the athlete's connected account. If it is not ACTIVE, get_authorization_link returns the Strava consent link the athlete opens once. Then strava_get_athlete confirms which Strava athlete the grant belongs to, which catches an athlete who approved from a partner's logged-in browser before your sync writes their activities to the wrong profile.

import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions CONNECTION_NAME = "strava" # must match the connection name in AgentKit > Connections identifier = "coachco_athlete_4821" # your app's stable ID for this athlete account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=CONNECTION_NAME, identifier=identifier, ) print("Authorize Strava:", link.link) input("Press Enter after approving access on Strava...") account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": raise RuntimeError(f"Strava is {account.connected_account.status}, not ACTIVE") me = actions.execute_tool( tool_name="strava_get_athlete", tool_input={}, connection_name=CONNECTION_NAME, identifier=identifier, ) print(me.execution_id, me.data.get("id") if isinstance(me.data, dict) else me.data)

Store the returned Strava athlete id against your identifier. Webhook deliveries identify athletes by owner_id, and this mapping is how a delivery finds the right connected account.

Retrieve the Strava tools this user can call

This is not a catalog load. The agent asks for the tools this athlete's connected account is authorized to call, then narrows them to the four a plan-alignment assistant needs. strava_update_athlete, strava_star_segment, and every stream tool stay out of the model's reach, so it cannot change an athlete's weight while renaming a run, and it never pulls a second-by-second heart rate array into context it does not need. The narrowing also keeps the data the model sees to the minimum, which matters on a platform with Strava's terms.

tools = actions.langchain.get_tools( identifier=identifier, connection_names=[CONNECTION_NAME], tool_names=[ "strava_list_activities", "strava_get_activity", "strava_update_activity", "strava_create_activity", ], page_size=100, ) tool_map = {t.name: t for t in tools} print(sorted(tool_map))

Run the agent loop

This loop requires Strava's agreement to AI use; under the published policy, the strava_list_activities results it hands to the model are "ingestion into a context window." With that agreement in place, the loop runs until the model stops requesting tools, every call executes with this athlete's grant, and the system prompt carries the details that trip models up on this API: epoch-second time filters, the gear_id prefixes, and the app-wide rate limit.

from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [ SystemMessage( "You keep one athlete's Strava activities aligned with the training plan their " "coach wrote. Use strava_list_activities with after and before as Unix epoch " "seconds and per_page no larger than 30. Use strava_get_activity only when a " "summary is not enough. Rename activities with strava_update_activity to match " "the plan's session names, and set gear_id only to IDs the plan provides; bikes " "start with b and shoes with g. Log sessions done without a device with " "strava_create_activity. Requests count against an app-wide rate limit shared " "by every athlete, so make as few calls as possible. Report exactly what you " "changed and never describe a change you did not make." ), HumanMessage( "This week's plan: Tuesday 'Threshold 3x10', Thursday 'Easy 45' in shoes " "g18273645, Saturday 'Long run 26k'. Wednesday was a 50 minute gym session, " "'Strength: lower body', with no watch. Make Strava match the plan for " "November 3 to November 9, 2026." ), ] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: tool = tool_map.get(tc["name"]) if tool: result = tool.invoke(tc["args"]) else: result = f"Unknown tool: {tc['name']}" messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

Deterministic calls with execute_tool

For pipelines, skip the model. execute_tool runs one tool with a fixed input and returns data plus an execution_id you can log. This is the part of a Strava integration that works under the published terms: no model, no context window, data shown back to the athlete who owns it, and a cache that expires.

The handler below covers Strava's webhook end to end. It answers the subscription handshake, ignores deliveries for any subscription but yours, treats a deauthorization as a purge, deletes cached activities when Strava says they were deleted or went private, and otherwise reads the activity with the owner's own grant. Because Strava signs nothing, the payload only ever supplies IDs; a forged delivery can at most make you read an activity that athlete already authorized you to see.

import json import os from datetime import datetime, timedelta, timezone STRAVA_VERIFY_TOKEN = os.environ["STRAVA_VERIFY_TOKEN"] STRAVA_SUBSCRIPTION_ID = int(os.environ["STRAVA_SUBSCRIPTION_ID"]) # Strava athlete id (owner_id) to your app's identifier, saved after authorization ATHLETE_IDENTIFIERS = {12345678: "coachco_athlete_4821"} # Transient cache keyed by activity id; section 6.2 caps retention at seven days ACTIVITY_CACHE: dict[int, dict] = {} CACHE_TTL = timedelta(days=7) def handle_strava_verification(query: dict) -> tuple[int, str]: """GET handler for the push subscription handshake; must answer within two seconds.""" if query.get("hub.mode") != "subscribe" or query.get("hub.verify_token") != STRAVA_VERIFY_TOKEN: return 403, "" return 200, json.dumps({"hub.challenge": query.get("hub.challenge", "")}) def purge_athlete(owner_id: int) -> None: for activity_id in [k for k, v in ACTIVITY_CACHE.items() if v["owner_id"] == owner_id]: ACTIVITY_CACHE.pop(activity_id, None) def handle_strava_event(raw_body: bytes) -> None: """Runs after the POST handler has already returned 200 to Strava.""" event = json.loads(raw_body) if event.get("subscription_id") != STRAVA_SUBSCRIPTION_ID: return owner_id = int(event.get("owner_id", 0)) object_type = event.get("object_type") aspect_type = event.get("aspect_type") updates = event.get("updates") or {} if object_type == "athlete" and str(updates.get("authorized", "")).lower() == "false": purge_athlete(owner_id) ATHLETE_IDENTIFIERS.pop(owner_id, None) # stop calling for this athlete return if object_type != "activity": return activity_id = int(event["object_id"]) if aspect_type == "delete": ACTIVITY_CACHE.pop(activity_id, None) return athlete_identifier = ATHLETE_IDENTIFIERS.get(owner_id) if athlete_identifier is None: return # not a connected athlete, or already deauthorized activity = actions.execute_tool( tool_name="strava_get_activity", tool_input={"id": activity_id}, connection_name=CONNECTION_NAME, identifier=athlete_identifier, ) ACTIVITY_CACHE[activity_id] = { "owner_id": owner_id, "data": activity.data, "expires_at": datetime.now(timezone.utc) + CACHE_TTL, } print(activity.execution_id, aspect_type, activity_id)

Strava's retries and its habit of emitting several events for one save mean the same activity can arrive more than once, so the cache write is keyed on activity_id and repeats are harmless. Pair the handler with an expiry sweep, so nothing outlives the seven-day limit even when an athlete never edits the activity again.

def sweep_expired_activities() -> int: now = datetime.now(timezone.utc) expired = [k for k, v in ACTIVITY_CACHE.items() if v["expires_at"] <= now] for activity_id in expired: ACTIVITY_CACHE.pop(activity_id, None) print(f"Removed {len(expired)} cached activities past the seven-day limit") return len(expired)

Run the sweep hourly. In production the cache is Redis or a database table with a TTL rather than a dictionary, but the rule is the same.

Reaching the Strava REST API

The webhook subscription is one per app, created once, and it belongs in your own deployment code rather than a tool. So does revocation when an athlete deletes their account in your product. Both authenticate with your app's client credentials, not an athlete token. Scalekit custom connectors cover authenticated per-athlete REST calls the prebuilt tools don't, such as POST /uploads.

import os import requests STRAVA_API = "https://www.strava.com/api/v3" CLIENT_ID = os.environ["STRAVA_CLIENT_ID"] CLIENT_SECRET = os.environ["STRAVA_CLIENT_SECRET"] # never ship this to a browser or an LLM existing = requests.get( f"{STRAVA_API}/push_subscriptions", params={"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, timeout=30, ) existing.raise_for_status() if existing.json(): print("Subscription already exists:", existing.json()[0]["id"]) else: created = requests.post( f"{STRAVA_API}/push_subscriptions", data={ "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "callback_url": "https://integrations.example.com/hooks/strava", "verify_token": os.environ["STRAVA_VERIFY_TOKEN"], }, timeout=30, ) if created.status_code == 429: raise RuntimeError("Strava rate limit hit; wait for the next 15-minute window") created.raise_for_status() print("Created subscription:", created.json()["id"]) def revoke_strava_token(token: str, token_type_hint: str = "refresh_token") -> None: """Revoking a refresh token also revokes its access tokens, and vice versa.""" response = requests.post( "https://www.strava.com/oauth/revoke", auth=(CLIENT_ID, CLIENT_SECRET), data={"token": token, "token_type_hint": token_type_hint}, timeout=30, ) if response.status_code == 503: raise RuntimeError("Strava revoke unavailable; safe to retry") response.raise_for_status()

The callback must already be serving handle_strava_verification when you run the POST, because Strava validates it during creation. Store the subscription ID as STRAVA_SUBSCRIPTION_ID for the event handler.

Multi-tool, multi-tenant Strava agents on a virtual MCP server

A coaching agent rarely stops at Strava. It reads training, then messages the coach in Slack. Load all 28 strava tools and the model holds strava_update_athlete and strava_create_activity while answering "did Ana run on Thursday." At roughly 200 tokens each, 28 tools is about 5,600 tokens before the agent does any work, and stream tools can return tens of thousands of samples per call on top of that. The fix is not better prompting. It is surface reduction. A Scalekit virtual MCP server declares exactly which tools an agent role can see across connections, and session tokens keep each run bound to one user's connected accounts. Docs: set up a virtual MCP server.

On Strava, one caveat comes first. A virtual MCP server that includes Strava tools is an MCP endpoint exposing the Strava API, which is the mechanism section 5.16(b) names. The configuration below is what a team with Strava's agreement would deploy; without one, keep Strava in the deterministic pipeline above and give the virtual MCP server only your other connections.

Create the server once per agent role

from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping vmcp = actions.mcp.create_config( name="strava-plan-alignment", description="Aligns an athlete's Strava activities with the coach's plan and notifies the coach in Slack", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="strava", tools=[ "strava_list_activities", "strava_update_activity", "strava_create_activity", ], ), McpConfigConnectionToolMapping( connection_name="slack", tools=["slack_send_message"], ), ], ) print(vmcp.config.id, vmcp.config.mcp_server_url)

Mint a session token per user, per run

One server definition serves every athlete. Before each run, check that the user's accounts are active and mint a short-lived session token scoped to them. Any MCP client that speaks Streamable HTTP can connect; this example uses langchain-mcp-adapters (pip install langchain-mcp-adapters) and passes the token as a Bearer header.

import asyncio from datetime import timedelta from langchain_mcp_adapters.client import MultiServerMCPClient config_id = vmcp.config.id mcp_server_url = vmcp.config.mcp_server_url user_id = "coachco_athlete_4821" accounts = actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier=user_id, include_auth_link=True, ) not_ready = [ a for a in accounts.connected_accounts if str(a.connected_account_status).upper() != "ACTIVE" ] for a in not_ready: print(f"{a.connection_name} is {a.connected_account_status}: {a.authentication_link}") if not_ready: raise RuntimeError("Connect the accounts above before starting this run") session_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier=user_id, expiry=timedelta(minutes=30), ).token async def load_tools(): client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": mcp_server_url, "headers": {"Authorization": f"Bearer {session_token}"}, } } ) return await client.get_tools() mcp_tools = asyncio.run(load_tools()) print([t.name for t in mcp_tools])

The agent sees four tools instead of 28 plus everything Slack exposes. The endpoint is static; the identity is per-user. This differs from the Strava MCP in exactly the way the policy cares about: Strava's server acts for the subscriber who connected their own AI client, while this one acts for whichever athlete your backend mints a token for. That is a product capability, and on Strava it is one that needs Strava's sign-off.

Observability: every Strava action tied to a user

Every execute_tool call returns an execution_id, and every execution is attributed to a connected account, which maps to one athlete. When an athlete asks why their Tuesday run was renamed "Threshold 3x10" or why a gym session appeared on their feed, the answer is a lookup, not an investigation.

On Strava, attribution carries extra weight because the terms are specific. If Strava asks how your app used an athlete's data, a log keyed by athlete that shows which tools ran, when, and from which pipeline is the evidence that the sync path stayed deterministic, that reads used the owner's own grant, and that deauthorized athletes stopped receiving calls the moment their event arrived. A shared credential makes that impossible to show: every request looks like the token's owner made it. Shared credentials are a single-user solution. They do not survive a second user.

Strava's own records will not fill the gap. Webhook deliveries name the activity's owner_id, not the process that changed it, and edits your app makes through activity:write look to Strava like the athlete's own. Per-request attribution lives in your logs or nowhere.

Recommended reading: Audit trails for agent auth

Which one to build against

If the person asking is an athlete analyzing their own training, the Strava MCP is the right answer and the one Strava's terms authorize: hosted, read-only, and included with the subscription. If you are building a product that serves many athletes, the Strava API is the only surface that supports it, and as of the June 1, 2026 policy it supports the non-AI half: per-athlete OAuth with narrow scopes, webhook sync, writes, and data shown back to its owner with a seven-day cache. Putting a model on that data, or exposing Strava tools through any MCP server, is what sections 5.3 and 5.16 prohibit, so that part of the roadmap starts with Strava rather than with code. Either way, you hold a rotating, six-hour Strava credential per athlete, and that's what needs production-grade infrastructure.

FAQs

Does Strava have an official MCP server?

Yes. Strava hosts a read-only MCP server that launched with Claude in June 2026 and with Google Gemini in the US in October 2026. Athletes sign in with OAuth, and the server can read activity history, streams, zones, gear and best efforts but cannot upload, edit or delete activities. It is included only with a Strava subscription.

Can I build a multi-athlete agent on the Strava MCP server?

No. Strava's API Policy authorizes the MCP server only for a subscriber's personal use of their own data and says it may not enable commercial or third-party access. Your backend cannot call it on an athlete's behalf, so it serves individual athletes in their own AI client, not a product.

What does the June 2026 Strava API Policy say about AI?

Section 5.3 bars using the API or Strava data in connection with developing, training, evaluating or operating any AI application, and names ingestion into a context window and retrieval-augmented generation. Section 5.16 bars operating an MCP server or agent interface that exposes the API. The only carve-out is the Strava MCP.

What can a product still build on the Strava API?

The non-AI half: per-athlete OAuth with narrow scopes, webhook-driven activity sync, data displayed back to the athlete who owns it, deauthorization handling, writes such as renaming activities or setting gear, and a cache that expires within seven days. Putting Strava data in front of a model needs an agreement with Strava first.

How do Strava OAuth tokens work?

Access tokens expire six hours after they are created. A refresh may return a new refresh token, and the old one is invalidated immediately, so two workers refreshing the same athlete at once can leave one holding a dead token. Revocation moves to a dedicated revoke endpoint, which becomes the only option on June 1, 2027.

How does Scalekit connect an application to Strava?

Scalekit's Strava connector runs the OAuth flow through your own Strava app, stores each athlete's token in a vault and refreshes it centrally, so the rotation race lives in one place. It exposes 28 tools, 24 read and 4 write, which you can call deterministically from a sync pipeline without a model in the loop.

Build your Strava agent with Scalekit

Browse the Scalekit Strava connector and the Strava connector docs with the full tool list. See Scalekit pricing, or talk to us.

No items found.
On this page
Share this article
Scalekit
Find us on ProductHunt

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.