# Strava Data Scraper - Clubs, Athletes & Challenges (`abotapi/strava-public-data-scraper`) Actor

Scrape public Strava data: clubs (members, location, sport, settings), athlete profiles and every current challenge (goals, dates, participants, qualifying activities). Club directory discovery, monitoring (NEW, UPDATED, EXPIRED), incremental runs, resume, MCP export.

- **URL**: https://apify.com/abotapi/strava-public-data-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 strava records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Strava Data Scraper

Collect public Strava data: every current challenge, club profiles with member previews, and athlete profiles - in one structured dataset with change tracking for scheduled monitoring.

### Why This Scraper?

- **All three public surfaces in one actor.** Challenges (goals, dates, participants, qualifying activities), clubs (member counts, locations, sport types, settings), and athlete profiles (name, badge, country) come out of the same run engine with the same record shape.
- **Four collection modes.** Pull the full current challenge gallery with nothing but defaults, paste club/athlete links, or walk the public club directory (hundreds of thousands of club ids) from any offset.
- **Richer club data than the directory page shows.** Each club record carries the description, sport type, club types, exact member count, invite-only and leaderboard settings, website, imagery, and the first member previews with their badge tiers.
- **Built for monitoring.** Incremental mode classifies every record NEW, UPDATED, UNCHANGED or EXPIRED across scheduled runs, participant counts and member previews never flag phantom changes, and every run can resume from an earlier run.
- **Structured and honest.** Every record carries the stable fields documented below, with imagery URLs and change tracking, and no placeholder rows.

### Use Cases

- **Who:** Sports-market analysts tracking which challenges brands sponsor each month and how big their audiences get.
- **Who:** Community managers benchmarking running and cycling clubs by size, location, sport and open-vs-invite settings.
- **Who:** Researchers studying athlete visibility: badge tiers (free vs subscribed) and public location distributions.
- **Who:** Sellers of training services building a directory of clubs to partner with, filtered by sport and country.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `recordId` | `club:1234567` |
| `kind` | `club`, `athlete` or `challenge` |
| `name` | `Riverside Runners` |
| `url` | `https://www.strava.com/clubs/1234567` |
| `description` | `A friendly community running club` |
| `sportType` | `Running` |
| `clubTypes` | `["CasualClub"]` |
| `city` / `state` / `country` / `countryCode` | `Springfield` / `Illinois` / `United States` / `US` |
| `memberCount` | `128` |
| `verified` / `inviteOnly` / `leaderboardEnabled` / `postsDefaultView` | club settings booleans |
| `website` | `https://example.org` |
| `avatarUrl` / `coverPhotoUrl` / `displayIcon` | club imagery and icon token |
| `memberPreviews` / `badgeType` / `profileVisibilitySetting` | first members with id, name, badge; `Premium`; `Everyone` |
| `challengeId` / `slug` / `shortName` / `subtitle` | `1234567` / `october-10k-challenge` / `October 10K` / `Lace 'em up` |
| `sport` | `run`, `ride`, `swim`, `walk`, `hike`, `workout` |
| `goal` / `dimension` / `activityType` | `10000.0` / `avg_pace_min_distance` / `streak` |
| `dateStart` / `dateEnd` | `2026-10-01` / `2026-10-31` |
| `participantsCount` / `followersInChallenge` | `244063` / `null` |
| `qualifyingActivities` | `["Badminton", "Basketball"]` |
| `organizingClub` / `joined` / `ended` | the club that runs the challenge; join state |
| `overviewText` / `rewardsText` / `rulesText` / `additionalInfoText` | the challenge page's section texts |
| `changeType` | `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED`, `EXPIRED` |

Every record also keeps the raw source payload in `raw` for auditability.

### How to Use

#### Quick start

1. **Challenges** (default): just run it. Every current challenge in the gallery, with detail pages read for participant counts and qualifying activities.
2. **Clubs / Athletes**: switch the mode and paste links (`strava.com/clubs/...`, `strava.com/athletes/...`) or bare ids.
3. **Discover**: walk the public club directory from a starting offset to build a club list at scale.

**All current challenges (the default run):**

```json
{
  "mode": "challenges",
  "challengeSports": ["run", "ride", "swim", "walk", "hike", "workout"],
  "fetchChallengeDetails": true,
  "maxItems": 30
}
```

**A club and athlete watchlist:**

```json
{
  "mode": "clubs",
  "links": ["https://www.strava.com/clubs/1234567", "987654"],
  "maxItems": 10
}
```

**Discover clubs from the public directory:**

```json
{
  "mode": "discover",
  "discoverCount": 50,
  "discoverOffset": 0,
  "maxItems": 50,
  "incrementalMode": true,
  "stateKey": "club-directory-walk"
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("abotapi/strava-public-data-scraper").call(run_input={
    "mode": "challenges",
    "challengeSports": ["run", "ride"],
    "maxItems": 50,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_dataset_items():
    print(item["recordId"], item["name"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('abotapi/strava-public-data-scraper').call({
    mode: 'challenges',
    challengeSports: ['run', 'ride'],
    maxItems: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) console.log(item.recordId, item.name);
```

#### Resume and recurring updates

Turn **Incremental mode** on and schedule the actor: each run pushes only NEW or CHANGED records, a complete scan marks disappeared records EXPIRED, and **Resume from run** skips anything an earlier run already collected. Challenge participant counts and club member previews never flag phantom changes.

### Send results into your apps (MCP connectors)

Point the run at an MCP connector you have authorized under Apify, Settings, API & Integrations, and each collected record is pushed to it after the run: Notion as a rich page per record, other connectors as a best-effort write. Leave the field empty to skip the export entirely; it never changes the dataset output.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mcpConnectors` | array | `[]` | Connector IDs to push results into after the run. |
| `notionParentPageUrl` | string | \`\` | When pushing into a Notion connector, the parent page to write under. |
| `maxNotifyListings` | number | `50` | Cap on records sent to MCP connectors per run. |

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | select | `challenges` | Challenges (all current challenges), Clubs (from links), Athletes (from links), Discover (public directory walk). |
| `challengeSports` | multi-select | all | Which sport sections of the challenge gallery to collect. |
| `fetchChallengeDetails` | checkbox | `true` | Adds participant count, qualifying activities and description per challenge. |
| `links` | list | one example of each shape | Club or athlete links (full URLs with ids or slugs, or bare ids/slugs); the default carries one example of every supported shape. |
| `discoverCount` | number | `20` | How many clubs to collect from the public directory, in directory order. |
| `discoverOffset` | number | `0` | Skip this many directory entries first (continue long walks in later runs). |
| `maxItems` | number | `20` | Maximum records to push. |
| `proxyConfiguration` | proxy | residential | A residential connection is the reliable default. |
| `resumeFromRunId` | string | (empty) | Skip records already collected in an earlier run. |
| `incrementalMode` | checkbox | `false` | Only NEW/CHANGED rows on re-runs; disappeared records become EXPIRED. |
| `stateKey` | string | (empty) | Name the incremental baseline yourself to share it across runs. |
| `emitUnchanged` | checkbox | `false` | Push every record each run, flagged UNCHANGED. |
| `emitExpired` | checkbox | `false` | Push records that disappeared from a complete scan, flagged EXPIRED. |
| `ignoreFieldsForChanges` | list | (empty) | Extra fields that should never mark a record as changed. |
| `mcpConnectors` | list | (empty) | Connector IDs to push results into after the run. |
| `notionParentPageUrl` | string | (empty) | When pushing into a Notion connector, the parent page to write under. |
| `maxNotifyListings` | number | `50` | Cap on records sent to MCP connectors per run. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

**A club record as it lands in the dataset:**

```json
{
  "recordId": "club:1234567",
  "kind": "club",
  "clubId": "1234567",
  "name": "Riverside Runners",
  "url": "https://www.strava.com/clubs/1234567",
  "description": "A friendly community running club",
  "sportType": "Running",
  "clubTypes": ["CasualClub"],
  "city": "Springfield",
  "state": "Illinois",
  "country": "United States",
  "countryCode": "US",
  "memberCount": 128,
  "verified": false,
  "inviteOnly": false,
  "leaderboardEnabled": true,
  "website": "https://example.org",
  "avatarUrl": "https://example.com/avatar.jpg",
  "coverPhotoUrl": "https://example.com/cover.jpg",
  "memberPreviews": [
    {
      "athleteId": "7654321",
      "firstName": "Jordan",
      "lastName": "Smith",
      "badgeType": "Premium",
      "profileImageUrl": "https://example.com/jordan.jpg"
    }
  ],
  "changeType": "NEW",
  "changedFields": ["clubId", "name", "url"],
  "firstSeenAt": "2026-10-24T10:00:00+00:00",
  "lastSeenAt": "2026-10-24T10:00:00+00:00"
}
```

The dataset ships Output tabs: Records (all kinds), Clubs, Athletes, Challenges, and Changes (incremental mode).

### Plan Requirement

This Actor charges per record collected plus a small surcharge for each challenge detail page read. Check the pricing tab for current per-event prices. Runs are typically a few cents: the default 20-item run reads one index call plus one detail page per challenge.

### FAQ

#### How much does it cost?

You pay the per-record event for each club, athlete or challenge record plus the detail surcharge for each challenge detail page read, and Apify compute for the run. A default 20-challenge run reads 1 index call plus 20 detail pages.

#### Is it legal to scrape Strava?

This actor reads Strava's public pages at modest request rates and documents its scope honestly. You remain responsible for how you use the data and for complying with Strava's terms and applicable law in your jurisdiction.

#### Can I get only new or changed records on a schedule?

Yes, that is what incremental mode is for. Turn it on and schedule the actor; each run pushes only NEW or CHANGED records (or everything, with "Also emit unchanged records"), and a complete scan marks disappeared records EXPIRED.

#### Why did my run fail instead of returning an empty dataset?

The site occasionally refuses individual proxy exits; the actor rotates to a fresh exit automatically. If every exit is refused (rare), the run fails loudly rather than returning empty results, so just run it again.

#### Can I use it with AI agents or MCP?

Yes. The output is structured JSON with stable field names, works with the Apify MCP server, and can be pushed straight into Notion or other apps through MCP connectors.

### 🔗 Want more sports data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🥦 <a href="https://apify.com/abotapi/lidl-es-scraper"><b>Lidl.es Products, Weekly Offers, Prices &amp; Reviews Scraper</b></a><br>Scrape Lidl.es grocery and non-food products across weekly offers, tools, home, garden...</td><td>⚽ <a href="https://apify.com/abotapi/sofascore-scraper"><b>SofaScore Scraper ⚽ Live Scores, Stats, Players &amp; Odds</b></a><br>Pull structured sports data from SofaScore across football, basketball, tennis, and 20+...</td></tr>
<tr><td>⚽ <a href="https://apify.com/abotapi/hotstar-com-scraper"><b>JioHotstar Scraper</b></a><br>Scrape the JioHotstar catalog by content type or URL. Extract shows, movies, episodes...</td><td>⚽ <a href="https://apify.com/abotapi/fotmob-scraper"><b>FotMob Scraper ⚽ Match Details, Stats, Lineups &amp; Players</b></a><br>Pull structured football data from FotMob: match details with scores, team stats, events...</td></tr>
<tr><td>⚽ <a href="https://apify.com/abotapi/bookmyshow-scraper"><b>BookMyShow Scraper</b></a><br>Scrape BookMyShow movies and live events by city, category or URL. Extract genres...</td><td>⚽ <a href="https://apify.com/abotapi/sportsbook-odds-scraper"><b>Sportsbook Odds Scraper (1xBet, Melbet, Linebet, Paripulse)</b></a><br>Collect live and prematch betting odds from 1xBet, Melbet, Linebet and Paripulse. Give it...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/strava-public-data-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Challenges collects every current Strava challenge (nothing else needed). Clubs and Athletes collect the links you paste. Discover walks the public club directory from a starting offset.

## `challengeSports` (type: `array`):

Which sections of the challenge gallery to collect (the promoted flagship challenge plus the six sport sections).

## `fetchChallengeDetails` (type: `boolean`):

Adds the participant count, qualifying activities and description for each challenge (one extra page read per challenge).

## `links` (type: `array`):

Club profile links (ids or slugs both work, e.g. strava.com/clubs/23629 or strava.com/clubs/ambleside-ac), athlete profile links (e.g. strava.com/athletes/2619603), or bare ids/slugs. The default carries one example of every supported shape; links that do not match the selected mode are ignored with a warning.

## `discoverCount` (type: `integer`):

How many clubs to collect from the public club directory, in directory order. Existing clubs fill the run; retired club ids answer 'not found' and are skipped.

## `discoverOffset` (type: `integer`):

Skip this many directory entries first (use it to continue a long discovery in later runs).

## `maxItems` (type: `integer`):

Maximum number of records to push.

## `proxyConfiguration` (type: `object`):

Residential is the reliable default. A datacenter connection also serves the site, but a few datacenter exits are refused per request — those runs rotate to a fresh exit automatically. Apify residential proxies are not available on free plans.

## `resumeFromRunId` (type: `string`):

Provide a run ID to resume: records already collected in that run are skipped. Example: e.g. iQKYh2M6PiGE6H6xh

## `incrementalMode` (type: `boolean`):

Only report NEW or CHANGED records on scheduled re-runs and mark disappeared ones EXPIRED. Participant counts and member previews never flag a record as changed.

## `stateKey` (type: `string`):

Name the incremental baseline yourself to share one baseline across several differently-configured runs. Leave empty to derive it from the run's scope. Example: e.g. my-club-watchlist

## `emitUnchanged` (type: `boolean`):

Push every record each run, flagged UNCHANGED, instead of only new or changed ones.

## `emitExpired` (type: `boolean`):

Push records that disappeared from a complete scan, flagged EXPIRED.

## `ignoreFieldsForChanges` (type: `array`):

Extra record fields that should never mark a record as changed. Participant counts and member previews are already ignored.

## `mcpConnectors` (type: `array`):

Connector IDs from Apify MCP Connector Dashboard to push results into.

## `notionParentPageUrl` (type: `string`):

When pushing into a Notion MCP connector, the parent page to write under. Example: https://www.notion.so/...

## `maxNotifyListings` (type: `integer`):

Cap on records sent to MCP connectors per run.

## Actor input object example

```json
{
  "mode": "challenges",
  "challengeSports": [
    "promoted",
    "run",
    "ride",
    "swim",
    "walk",
    "hike",
    "workout"
  ],
  "fetchChallengeDetails": true,
  "links": [
    "https://www.strava.com/clubs/23629",
    "https://www.strava.com/clubs/ambleside-ac",
    "23629",
    "ambleside-ac",
    "https://www.strava.com/athletes/2619603"
  ],
  "discoverCount": 20,
  "discoverOffset": 0,
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `clubs` (type: `string`):

No description

## `athletes` (type: `string`):

No description

## `challenges` (type: `string`):

No description

## `changes` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "challenges",
    "challengeSports": [
        "promoted",
        "run",
        "ride",
        "swim",
        "walk",
        "hike",
        "workout"
    ],
    "links": [
        "https://www.strava.com/clubs/23629",
        "https://www.strava.com/clubs/ambleside-ac",
        "23629",
        "ambleside-ac",
        "https://www.strava.com/athletes/2619603"
    ],
    "discoverCount": 20,
    "discoverOffset": 0,
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    },
    "maxNotifyListings": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/strava-public-data-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "challenges",
    "challengeSports": [
        "promoted",
        "run",
        "ride",
        "swim",
        "walk",
        "hike",
        "workout",
    ],
    "links": [
        "https://www.strava.com/clubs/23629",
        "https://www.strava.com/clubs/ambleside-ac",
        "23629",
        "ambleside-ac",
        "https://www.strava.com/athletes/2619603",
    ],
    "discoverCount": 20,
    "discoverOffset": 0,
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
    "maxNotifyListings": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/strava-public-data-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "challenges",
  "challengeSports": [
    "promoted",
    "run",
    "ride",
    "swim",
    "walk",
    "hike",
    "workout"
  ],
  "links": [
    "https://www.strava.com/clubs/23629",
    "https://www.strava.com/clubs/ambleside-ac",
    "23629",
    "ambleside-ac",
    "https://www.strava.com/athletes/2619603"
  ],
  "discoverCount": 20,
  "discoverOffset": 0,
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "maxNotifyListings": 50
}' |
apify call abotapi/strava-public-data-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/strava-public-data-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/eube9ZnEpZXa3L1ZB/builds/M0MrNde7zZ6rup7r6/openapi.json
