Skip to main content

strava

CI codecov PyPI Python PyPI - Downloads License

A modern, fully-typed Python SDK for the Strava API v3.

Features

  • Sync and async clients built on httpx
  • Full type annotations and py.typed support
  • 34 API endpoints across 10 resource groups (31 main API endpoints + 3 webhook endpoints)
  • 50+ dataclass models with automatic serialization
  • OAuth2 authentication with automatic token refresh
  • Lazy pagination iterators
  • Custom exception hierarchy with rate limit details

SportType includes Strava's April 2026 additions: BASKETBALL, CRICKET, DANCE, PADEL, PHYSICAL_THERAPY, and VOLLEYBALL.

Installation

pip install strava

Quick Start

from strava import Strava

with Strava(access_token="your_token") as client:
    # Get authenticated athlete
    athlete = client.athletes.retrieve_authenticated()
    print(f"{athlete.firstname} {athlete.lastname}")

    # List recent activities
    for activity in client.activities.list(per_page=10):
        print(f"{activity.name} - {activity.distance}m")
        print(activity.device_name)  # Recording device, when supplied by Strava

Async

from strava import AsyncStrava

async with AsyncStrava(access_token="your_token") as client:
    athlete = await client.athletes.retrieve_authenticated()
    activities = await client.activities.list(per_page=10).collect()

Supplying an HTTPX client

Pass http_client=httpx.Client(...) to Strava, or an httpx.AsyncClient to AsyncStrava, to reuse a transport, connection pool, event hooks, and custom default headers. The SDK applies its own settings to each resource request:

  • access_token and the SDK's OAuth refresh credentials control authentication, overriding the supplied client's auth handler and default Authorization header. Webhook requests disable token authentication and remove Authorization entirely.
  • base_url controls the API destination, including its path prefix. Its default is https://www.strava.com/api/v3, even if the supplied client has another URL.
  • timeout controls requests and automatic OAuth refresh, defaulting to 30 seconds even if the supplied client has another timeout. OAuth refresh uses Strava's token endpoint independently of the API base URL.

The SDK does not mutate the supplied client's base_url, timeout, auth, or default headers. Other custom default headers are retained on resource requests. You own the supplied client: SDK close() and context exit leave it open. SDK-created HTTP clients are closed by the SDK.

import httpx
from strava import Strava

with httpx.Client(headers={"X-App": "my-app"}) as http:
    with Strava(access_token="your_token", timeout=10.0, http_client=http) as client:
        athlete = client.athletes.retrieve_authenticated()
    # `http` remains open until its own context exits.

The same ownership rules apply to AsyncStrava; use async with for both clients or close the supplied client yourself with await http.aclose().

Migration: Previously, a supplied HTTPX client's auth, base URL, and timeout could take precedence, and closing the SDK closed that client. Move API URL and timeout overrides to the SDK constructor, provide token and refresh settings to the SDK, and explicitly manage the supplied HTTPX client's lifetime.

OAuth2 Authentication

Get an authorization URL

from strava import build_authorization_url

url = build_authorization_url(
    client_id="your_client_id",
    redirect_uri="http://localhost:8000/callback",
    scopes=["read", "activity:read_all"],
)
# Redirect the user to `url`

Exchange the code for tokens

from strava import exchange_token

tokens = exchange_token(
    client_id="your_client_id",
    client_secret="your_secret",
    code="code_from_callback",
)
print(tokens.access_token, tokens.refresh_token, tokens.expires_at)
print(tokens.scope)  # Granted scopes, e.g. "activity:read activity:write"
if tokens.athlete is not None:
    print(tokens.athlete.id, tokens.athlete.firstname)

TokenResponse.scope preserves the space-delimited scopes actually granted by the athlete; these can differ from the requested scopes. TokenResponse.athlete contains a typed SummaryAthlete when returned. Both fields default to None when omitted, including in token refresh responses.

Refresh a token manually

from strava import refresh_access_token

tokens = refresh_access_token(
    client_id="your_client_id",
    client_secret="your_secret",
    refresh_token="current_refresh_token",
)
print(tokens.access_token, tokens.refresh_token, tokens.expires_at)

Automatic token refresh

from strava import Strava


def save_tokens(access_token, refresh_token, expires_at):
    # Persist the new tokens to your database
    ...


client = Strava(
    access_token="...",
    client_id="your_client_id",
    client_secret="your_secret",
    refresh_token="...",
    expires_at=1700000000,
    on_token_refresh=save_tokens,
)

If automatic refresh fails with an HTTP error, the SDK raises the error from the OAuth response before sending the original API request. Tokens remain unchanged and on_token_refresh is not called. This applies to both sync and async clients.

Revoke tokens

from strava import revoke_token

revoke_token(
    client_id="your_client_id",
    client_secret="your_secret",
    token="access_or_refresh_token",
    token_type_hint="access_token",
)

deauthorize(access_token=...) remains available for compatibility, but it is deprecated because Strava will retire oauth/deauthorize on June 1, 2027. Prefer revoke_token() for new code.

API Coverage

Resource Methods
Activities create, retrieve, update, list, list_comments, list_kudoers, list_laps, list_zones
Athletes retrieve_authenticated, update_authenticated, retrieve_zones, retrieve_stats
Clubs retrieve, list_authenticated
Gear retrieve
Routes retrieve, export_gpx, export_tcx, list_by_athlete
Segments retrieve, explore, list_starred, star
Segment Efforts retrieve, list
Streams get_activity_streams, get_route_streams, get_segment_effort_streams, get_segment_streams
Uploads create, retrieve
Webhooks create, list, delete

Activity and segment response fields

Both SummaryActivity and DetailedActivity expose optional resource_state, has_heartrate, average_heartrate, max_heartrate, average_cadence, average_temp, pr_count, suffer_score, and utc_offset (seconds) fields. Both SummarySegment and DetailedSegment expose optional resource_state, starred, and hazardous fields. Omitted or null fields default to None; to_dict() omits None but preserves False and zero values.

athlete_segment_stats remains a SummarySegmentEffort on both segment models. It preserves the documented effort fields (id, activity_id, elapsed_time, start_date, start_date_local, distance, and is_kom) and also accepts the PR fields returned by Strava's reference examples: pr_elapsed_time, pr_date, and effort_count, plus optional pr_activity_id. Responses can contain either shape or both together, without losing fields or changing isinstance(stats, SummarySegmentEffort) compatibility. athlete_pr_effort continues to use SummaryPRSegmentEffort.

pr_date is parsed as a UTC-aware datetime, consistent with other model dates; a date-only value such as "1993-04-03" becomes midnight UTC. to_dict() emits an ISO timestamp with a UTC offset. These fields are based on the official API reference, whose segment stats sample and schema use different shapes; both are supported.

Streams

All four stream methods return a typed StreamSet, accepting both responses keyed by stream type and legacy lists of stream objects. Empty responses produce an empty StreamSet; unknown stream types are ignored. Keyed stream values do not need an inner type field.

with Strava(access_token="your_token") as client:
    streams = client.streams.get_activity_streams(123, keys=["time", "heartrate"])
    if streams.heartrate is not None:
        print(streams.heartrate.data)
    route_streams = client.streams.get_route_streams(456)

Activity, segment-effort, and segment stream requests send keys as a comma-separated string and key_by_type=true. Route stream requests send no query parameters. The same methods are available on AsyncStrava with await.

Compatibility note: key_by_type=False is no longer allowed and raises a ValueError before sending an HTTP request. Remove that argument or pass True; legacy list responses are still supported. StreamSet.from_stream_list() also remains available, and StreamSet.from_response() handles either response shape.

Webhook subscriptions

Manage subscriptions with your application's credentials. These requests bypass athlete-token authentication and automatic token refresh. Strava permits one subscription per application.

with Strava(
    access_token="your_token", base_url="https://www.strava.com/api/v3"
) as client:
    subscription = client.webhooks.create(
        client_id="your_client_id",
        client_secret="your_client_secret",
        callback_url="https://your-app.example/webhooks/strava",
        verify_token="your_verification_token",
    )
    subscriptions = client.webhooks.list(
        client_id="your_client_id", client_secret="your_client_secret"
    )
    client.webhooks.delete(
        subscription.id,
        client_id="your_client_id",
        client_secret="your_client_secret",
    )

Your callback must answer Strava's verification request within two seconds with the JSON body {"hub.challenge": "<received challenge>"}. The same methods are available on AsyncStrava with await. See the official webhook guide for callback and event handling.

Strength-training uploads

uploads.create() accepts sport_type as a SportType member or a string to override the sport detected from a file. If omitted, Strava uses file metadata.

Strava accepts JSON strength-training files for WeightTraining, HighIntensityIntervalTraining, Workout, and Crossfit. The sample examples/strength-training.json contains repetition-based and timed sets, weights in kilograms, and optional heart-rate and active-time streams.

from strava import SportType, Strava

with Strava(
    access_token="your_token", base_url="https://www.strava.com/api/v3"
) as client:
    with open("examples/strength-training.json", "rb") as file:
        upload = client.uploads.create(
            file=file,
            data_type="json",
            sport_type=SportType.WEIGHT_TRAINING,
            name="Strength session",
        )
    print(upload.id, upload.status)

Use data_type="fit" to upload a FIT strength-training file containing set messages. For example, replace the file above with strength.fit and the sport with SportType.CROSSFIT. The SDK transmits file bytes without modifying them. FIT files with sets do not need timestamped record messages, but must include the activity timestamp and session total elapsed time required by Strava.

JSON files require version "1.0", a timezone-aware start_time, utc_offset in seconds, elapsed_time, and at least one set. If streams are provided, time is required and all stream arrays must have equal lengths. These upload streams are part of the file format, separate from the read-only streams API. See the uploads guide for the complete specification and supported exercises.

Uploads require activity:write and are processed asynchronously. Poll uploads.retrieve(upload.id) no more than once per second until activity_id is populated or error is set. Async applications can use await client.uploads.create(...) and await client.uploads.retrieve(...).

Strava API Changes

The default API base URL is the current official https://www.strava.com/api/v3. Strava's future API host, https://api-v3.strava.com (without www), will be available starting January 4, 2027. The SDK keeps the current default and does not switch hosts automatically based on the clock. Once the future host is available, you can opt in explicitly with base_url="https://api-v3.strava.com" on either Strava or AsyncStrava.

Breaking change: retired club endpoints

Strava removed club activities, club members, and club administrators endpoints on September 1, 2026. The SDK has removed clubs.list_activities(), clubs.list_members(), and clubs.list_admins() from both Strava and AsyncStrava. Accessing these methods now raises AttributeError.

Migration: Remove calls to these methods and any application features that depend on them. Strava provides no documented replacement endpoints. clubs.retrieve() and clubs.list_authenticated() remain supported. ClubActivity and ClubAthlete models and their public exports remain available for parsing existing data.

Other methods are affected by Strava's 2026 Developer Program changes:

  • segments.explore() is restricted to approved Extended Access applications effective September 1, 2026.
  • deauthorize() is deprecated; use revoke_token() with your client credentials instead.

Pagination

List endpoints return lazy paginators:

# Iterate one item at a time (fetches pages on demand)
for activity in client.activities.list():
    print(activity.name)

# Collect all results eagerly
all_activities = client.activities.list().collect()

# Limit results
first_50 = client.activities.list().collect(max_items=50)

# Iterate page by page
for page in client.activities.list(per_page=50).pages():
    print(f"Got {len(page)} activities")

Page-number pagination continues until Strava returns an empty page, even if an intermediate page contains fewer than per_page items. No requests are made until iteration begins. collect(max_items=0) returns an empty list without a request; negative limits raise ValueError. A positive limit stops as soon as enough items have been collected, without fetching another page.

Comment cursors

activities.list_comments() uses cursor pagination and sends only page_size and, when present, after_cursor. The default page size is 30. per_page remains a backward-compatible alias for page_size; page_size wins when both are supplied.

comments = client.activities.list_comments(123, page_size=50)
for comment in comments:
    print(comment.text)

# Resume after a previously saved comment cursor, passed through unchanged.
next_comments = client.activities.list_comments(
    123, page_size=50, after_cursor=saved_cursor
).collect(max_items=20)

Each Comment exposes an optional cursor. The paginator uses the last raw comment's cursor to fetch the next page, preserving opaque tokens without decoding or modifying them. Short pages still continue until an empty response. If continuation requires a missing, invalid, repeated, or cycling cursor, it raises RuntimeError rather than looping indefinitely. Cursor validation is deferred until another page is needed, so bounded collection and stopping iteration after the current page do not raise unnecessarily.

Async paginators provide the same behavior via async for, async for page in paginator.pages(), and await paginator.collect(...), including for comments:

async for comment in async_client.activities.list_comments(123, page_size=50):
    print(comment.text)

comments = await async_client.activities.list_comments(123).collect(max_items=20)

Error Handling

from strava import (
    StravaError,
    AuthenticationError,
    AuthorizationError,
    NotFoundError,
    RateLimitError,
    ValidationError,
    ServerError,
)

try:
    activity = client.activities.retrieve(123)
except AuthenticationError:
    print("Authentication failed — check your credentials and tokens")
except AuthorizationError:
    print("Insufficient permissions")
except NotFoundError:
    print("Activity not found")
except ValidationError as e:
    print(f"Bad request: {e.message}")
except RateLimitError as e:
    print(f"Rate limited. 15-min usage: {e.usage_15min}/{e.limit_15min}")
except ServerError:
    print("Strava server error — try again later")
except StravaError as e:
    print(f"API error {e.status_code}: {e.message}")

API requests and OAuth operations (exchange_token(), refresh_access_token(), revoke_token(), deprecated deauthorize(), and automatic refresh) use the same SDK exception hierarchy:

HTTP status Exception
400, 422 ValidationError
401 AuthenticationError
403 AuthorizationError
404 NotFoundError
429 RateLimitError
5xx ServerError
Other 4xx StravaError

These exceptions preserve status_code, message, the original HTTP response, and the JSON fault dictionary when available. Non-JSON error responses use the response text as the message. RateLimitError also exposes the general and read rate-limit limits and usage from the error response headers.

A 401 maps to AuthenticationError; it does not reliably identify an expired token. TokenExpiredError remains exported as an AuthenticationError subclass for compatibility, but the SDK does not automatically raise it.

Compatibility change: OAuth HTTP failures now raise SDK exceptions instead of httpx.HTTPStatusError. Update handlers that catch httpx.HTTPStatusError for OAuth operations to catch StravaError or the appropriate subclass.

Development

# Install dependencies
uv sync

# Run tests
make test

# Run tests with coverage
make coverage

# Check package types and static consumer contracts
make typecheck

# Lint and format
make lint
make format

Strict mypy checks target Python 3.11 and cover src/strava plus tests/typing. The static consumer contracts verify public model, endpoint, paginator, and OAuth metadata types without executing API requests. Runtime tests are run with pytest and are outside the current mypy scope.

Python Version

Supports Python 3.11 through 3.14.

Contributing

Contributions are welcome! Please read the Contributing Guide before opening a pull request.

Found a bug? Open an issue. Have an idea? Request a feature.

Metadata

Release files for strava 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for strava 0.6.0
File Size Uploaded
strava-0.6.0.tar.gz 47.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for strava 0.6.0
File Interpreter ABI Platform
strava-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 99.3 kB

Release files / strava-0.6.0.tar.gz

Download URL strava-0.6.0.tar.gz
Size 47.3 kB
Tags Source
SHA-256 checksum
How to use checksums
ea5c726a1da4aecd546f381bae1b95b8981263cf03231e6a485ed386a35575f2
BLAKE2b-256 checksum
How to use checksums
1cadded9308f05827bdeabbd8486da5d29dfc4d2f6101f95d16ab8b0852010be
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / strava-0.6.0-py3-none-any.whl

Download URL strava-0.6.0-py3-none-any.whl
Size 52.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1fff61ef5e6f182da9c99e5edc58252abc0de9e26cb6e55565f5b4e57413553b
BLAKE2b-256 checksum
How to use checksums
ff17fa3130f16a3ad075d45723c42e13af97cf3d17163e786015bc9bf9970102
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page