# ProCyclingStats Scraper: Results & Riders (`recordsdata/procyclingstats-scraper`) Actor

Scrape ProCyclingStats race and stage results (rank, rider, team, age, UCI and PCS points, time), rider profiles with specialty points and the PCS world ranking. Export CSV, Excel, JSON or XML. No login or API key. Cloudflare handled with residential proxy.

- **URL**: https://apify.com/recordsdata/procyclingstats-scraper.md
- **Developed by:** [RecordsData](https://apify.com/recordsdata) (community)
- **Categories:** Sports, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $16.67 / 1,000 race result rows

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

<p align="center">
  <img src="https://api.apify.com/v2/key-value-stores/AAm3a1h3Z9nYfrvh9/records/banner?v=3" alt="RecordsData" width="100%" />
</p>

## 🚴 ProCyclingStats Scraper: Results, Startlists, Teams & Rankings

> **ProCyclingStats Scraper** extracts race and stage results by race name and year or by URL, startlists, team rosters, rider profiles with specialty points, and seven PCS and UCI ranking tables. Choose winner only, final classification or every stage, export CSV, Excel, JSON or XML. No login, no API key. Verified 2026-10-10. Race results cost $0.02 per row on the free tier; startlist and team roster riders cost $0.005 each.

The ProCyclingStats Scraper turns procyclingstats.com, the reference statistics site for professional road cycling, into spreadsheets and API-ready JSON. ProCyclingStats sits behind a Cloudflare gate that blocks ordinary scrapers, so this actor uses a browser-grade TLS client with Apify residential proxy by default. In our cloud test, Paris-Roubaix 2025 with winner only returned exactly one row.

### 🔎 What does the ProCyclingStats Scraper do?

- **Scrape ProCyclingStats race results by name**: give `raceName` and `year` and pick `resultScope`: `winnersOnly` (1 row), `finalClassification`, `stageWinners` or `allStages`. Stage races also take a `classification` (GC, points, mountains, youth) or a single `stage`. Or paste race, stage, startlist, team, rider and ranking URLs in `raceUrls`.
- **Download startlists**: riders with bib, nationality, team and team class. The 2025 Tour de France startlist returned 184 riders.
- **Download team rosters and rider profiles**: age, specialty, PCS rank, career points, team wins and ranks; birth data, height, weight and specialty points, from slugs, URLs or plain names.
- **Download seven ranking tables**: PCS riders, teams and nations, UCI riders and teams, women PCS and UCI riders, with an optional nation filter and up to 2,500 rows.
- **Mix modules in one run**, capped in total by `maxItems`.

Not available at the source: a ranking at a past date (PCS ignores the date parameter) and the UCI nations table (it is empty).

### 📊 What data can you extract from ProCyclingStats?

The dataset schema has 55 fields and six views (overview, results, startlist, team roster, riders, rankings). Rows carry only keys that have a value. The main ones:

| Field | Applies to | Example |
|---|---|---|
| `recordType` | all | `race-result`, `startlist`, `team-rider`, `rider`, `ranking` |
| `race`, `year`, `raceClass` | results, startlist | race name, season, class such as 2.UWT |
| `classification`, `stage`, `stageName`, `date` | results | `gc`, `5`, route, ISO date |
| `distanceKm`, `parcoursType`, `verticalMeters`, `avgSpeedKmh` | results | stage profile data |
| `rank`, `status`, `gcRank` | results | integer rank; status Finished, DNF, DNS, OTL, DSQ, DF |
| `rider`, `riderSlug`, `bib` | results, startlist, roster | rider name, PCS slug, start number |
| `nationalityCode`, `nationality` | results, startlist, rider | 2-letter code, country name |
| `team`, `teamSlug`, `teamClass` | results, startlist | team, slug with season, WT or PRT or CT |
| `age`, `specialty` | results, roster | age in race year, PCS specialty |
| `uciPoints`, `pcsPoints`, `classificationPoints` | results | points won in the race |
| `time`, `timeSeconds`, `timeBehind`, `gapSeconds` | results | `3:07:30`, gap text and numeric twins |
| `dateOfBirth`, `placeOfBirth`, `weightKg`, `heightM` | rider | profile data |
| `specialtyPoints`, `pcsRank`, `allTimeRank` | rider | object with Oneday, GC, TT, Sprint, Climber, Hills |
| `rankingPoints`, `careerPoints`, `teamWins`, `teamPoints`, `teamPcsRank`, `teamUciRank` | roster | snapshot at the scrape date |
| `previousRank`, `rankChange`, `points`, `nation`, `nationSlug` | ranking | decimals kept, such as 10331.8 |
| `url`, `raceUrl`, `scrapedAt` | all | source page and ISO timestamp |

If PCS does not show a value, the key is left out (no "N/A" placeholders). `rank` is always an integer; riders who did not finish have no `rank` and a `status` instead. A rider or team PCS does not know returns one free error row. Roster rank and points are a snapshot at the scrape date.

### 🧾 Sample output

Trimmed rows from earlier cloud runs.

Race result row (Tour de France 2025, stage 21):

```json
{
  "recordType": "race-result",
  "raceUrl": "https://www.procyclingstats.com/race/tour-de-france/2025/stage-21",
  "rank": 1,
  "rider": "van Aert Wout",
  "riderSlug": "wout-van-aert",
  "team": "Team Visma | Lease a Bike",
  "age": 30,
  "uciPoints": 210,
  "time": "3:07:30",
  "scrapedAt": "2026-10-04T03:59:14.626Z"
}
```

Rider profile row:

```json
{
  "recordType": "rider",
  "rider": "Tadej Pogačar",
  "riderSlug": "tadej-pogacar",
  "nationality": "Slovenia",
  "weightKg": "66",
  "heightM": "1.76",
  "specialtyPoints": { "Oneday": 9983, "GC": 8344, "TT": 3747, "Sprint": 297, "Climber": 11052, "Hills": 4654 },
  "scrapedAt": "2026-10-04T04:00:46.843Z"
}
```

### 💰 How much does it cost to scrape ProCyclingStats?

Pay-per-event pricing, live prices on the free plan tier; paid Apify plans pay less per race result row (down to $0.01667).

| Event | Price | What counts |
|---|---|---|
| Race result row | $0.02 | one classified rider, or one stage winner |
| Rider profile | $0.02 | one rider profile |
| Ranking row | $0.01 | one ranking position, any of the seven tables |
| Startlist rider | $0.005 | one rider entered in a race startlist |
| Team roster rider | $0.005 | one rider of a team season roster |

Worked example on the free tier: 1,000 race result rows cost 1,000 x $0.02 = $20.00, 1,000 rider profiles cost $20.00, and 1,000 ranking rows cost 1,000 x $0.01 = $10.00. A Paris-Roubaix 2025 winner-only run is one row, $0.02. The 2025 Tour de France startlist (184 riders) costs 184 x $0.005 = $0.92, and a 30-rider team roster costs 30 x $0.005 = $0.15. You are not billed for error rows, unknown races or riders, empty results or a run that fails. A request budget (10 requests before the first row, then 3 per delivered row) stops runs that find nothing.

### 🚀 How to scrape ProCyclingStats in 3 steps

1. Open the actor on Apify. Type a race name and a season, or paste race, startlist, team or rider URLs.
2. Pick a result scope, an optional ranking table, and set `maxItems`.
3. Click **Start**, then download the dataset as CSV, Excel, JSON or XML.

An empty input returns the Paris-Roubaix 2025 winner (1 row).

### ⚙️ ProCyclingStats Scraper input

| Field | Type | Default | Description |
|---|---|---|---|
| `raceName` | string | empty | Race by name, for example Paris-Roubaix |
| `year` | integer | latest | Season of the race |
| `resultScope` | string | finalClassification | winnersOnly, finalClassification, stageWinners, allStages |
| `classification` | string | gc | Stage race: gc, points, kom, youth |
| `stage` | integer | 0 | One stage of a stage race (0 is off) |
| `raceContent` | string | results | results or startlist |
| `raceUrls` | list | empty | Race, stage, startlist, team, rider or ranking URLs |
| `teamUrls` | list | empty | Team URLs, slugs with season, or names |
| `riderUrls` | list | empty | Rider slugs, URLs or plain names |
| `rankingType` | string | none | Seven PCS and UCI tables |
| `rankingNation` | string | empty | 2-letter nation code filter |
| `maxRankingRows` | integer | 100 | Ranking positions to keep, up to 2500 |
| `maxItems` | integer | 10 | Total row cap (free users: 10) |
| `proxyConfiguration`, `includeRankings` | object, boolean | residential, false | Proxy settings; legacy PCS riders ranking switch |

```json
{
  "raceName": "Paris-Roubaix",
  "year": 2025,
  "resultScope": "winnersOnly",
  "riderUrls": ["tadej-pogacar", "Wout van Aert"],
  "rankingType": "uci-individual",
  "maxRankingRows": 50,
  "maxItems": 200
}
```

Name lookup uses an alias list, a slug guess and, only if both miss, one PCS search (at most 6 requests). `raceUrls` now defaults to an empty list; it used to scrape Tour de France stage 21 in every run.

### 📦 ProCyclingStats Scraper output

Results go to the default dataset, with six table views in the Output tab. Download as JSON, CSV, Excel or XML. If a race, rider or team does not exist, the run still succeeds with one free error row or a "Finished with 0 results" message and nothing is charged. If ProCyclingStats blocks every request, the run fails with a message instead of reporting success.

### ⚖️ ProCyclingStats Scraper vs alternatives

Read on 2026-10-10:

| | This actor | trovevault | lexis-solutions |
|---|---|---|---|
| Race by name and year | Yes, plus URLs | Yes, dropdown or custom name | Not stated; URLs only |
| Result scopes | Winner, final, stage winners, all stages | Winners, classifications, stages, full race | Not stated in their listing |
| Startlists, rosters, rankings | Yes, seven ranking tables | Not stated in their listing | Pages by URL, columns vary |
| Output | 55 typed fields, 6 views | 17 fields | Rows keyed by page headers |
| Price per 1,000 rows | $20 Free tier, $16.67 top tier | From $0.85 | From $6.40 |

trovevault covers race results by name with a rank cap and is far cheaper per row; its listing shows no startlists, team rosters, rider profiles or ranking tables. lexis-solutions reads any PCS page you paste, with columns that depend on the page. We cost roughly 20 times trovevault on result rows, so if you only need results by race name at the lowest price, it may fit you better. What the extra buys here: startlists, rosters, rider specialty points, seven rankings and typed columns. The crawlerbros listing returned a 404 on 2026-10-10, so it is not compared.

### 🎯 ProCyclingStats data use cases

Fantasy cycling models, editorial race coverage, talent scouting with specialty-point profiles, scheduled ranking snapshots with previous rank and change, and race previews from startlists and rosters.

### 🔌 Run the ProCyclingStats Scraper via API and integrations

```bash
curl -X POST "https://api.apify.com/v2/acts/RecordsData~procyclingstats-scraper/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"raceName":"Paris-Roubaix","year":2025,"resultScope":"winnersOnly","maxItems":10}'
```

Use the Apify API client for Python or JavaScript, schedules, or connect results to Zapier, Make, n8n, Google Sheets or Airbyte.

### 🤖 ProCyclingStats Scraper MCP server for AI agents

AI agents can call ProCyclingStats Scraper through the Apify MCP server at `https://mcp.apify.com?tools=RecordsData/procyclingstats-scraper`.

Add this to your MCP client configuration (Claude Desktop, Cursor, VS Code or any client that supports remote MCP servers):

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=RecordsData/procyclingstats-scraper"
    }
  }
}
```

Example agent prompt:

> Use the ProCyclingStats Scraper for Paris-Roubaix 2025 with the winner only scope, then get the Tour de France 2025 startlist and the top 20 of the UCI individual ranking. Give me the winner, the Belgian riders on the startlist with their teams, and the ranking.

Tool description: choose ProCyclingStats Scraper when an agent needs structured professional road cycling data from procyclingstats.com: race results by race name and year, startlists, team rosters, rider specialty points, or PCS and UCI ranking tables.

### ⚖️ Is it legal to scrape ProCyclingStats?

The actor reads publicly visible race and rider statistics and does not log in or bypass paywalls. You are responsible for using the data in line with the site's terms and with local rules such as GDPR for personal data. This is an independent tool, not affiliated with ProCyclingStats.

### ❓ Frequently asked questions

#### How do I export ProCyclingStats results to CSV or Excel?

Type a race name and season, such as Paris-Roubaix and 2025, or paste the race or stage result URL into the raceUrls field of ProCyclingStats Scraper, then click Start. When the run finishes, open the Storage tab and download the dataset as CSV or Excel. JSON and XML are also available, as is the Apify API. Each classified rider becomes one row.

#### Does ProCyclingStats block scrapers?

Yes. procyclingstats.com sits behind a Cloudflare TLS check that blocks ordinary scrapers. ProCyclingStats Scraper uses a Firefox-grade TLS client with a session warm-up and the Apify residential proxy on by default, and this worked in our cloud runs. Keep the default proxy enabled.

#### Which ProCyclingStats races work with the scraper?

Any race ProCyclingStats lists works in ProCyclingStats Scraper: grand tours, one-day classics and national championships. Give the race name and season, or paste a URL such as a Paris-Roubaix 2025 page. Races with no classification yet return no rows and no charge.

#### What does the time column mean in ProCyclingStats Scraper output?

The time field is shown as on procyclingstats.com, such as 3:07:30, with the accumulated time on GC rows. The gap to the leader sits in timeBehind, with numeric twins timeSeconds and gapSeconds for sorting. If PCS shows no time for a rider, the key is left out of the row.

#### How are DNF and DNS riders shown by ProCyclingStats Scraper?

Through the status column, so no rider silently disappears from the classification. In ProCyclingStats Scraper, finishers have status Finished and an integer rank, while DNF, DNS, OTL, DSQ and DF riders have no rank and carry that status instead. Rank is always a number when present.

#### Why did ProCyclingStats Scraper return 0 results?

Either the URL does not exist or the race has no classification yet. ProCyclingStats Scraper then ends the run as succeeded with the status message "Finished with 0 results" and charges nothing. Check the URL in a browser and try again once PCS has published the classification.

#### Why did my ProCyclingStats Scraper run fail?

When every request to procyclingstats.com is blocked or fails, ProCyclingStats Scraper marks the run as failed with a message instead of reporting an empty success, and nothing is charged. Keep the residential proxy enabled in the input and retry. A single bad URL does not fail the run if other URLs delivered rows.

#### Do I pay for rows that failed in ProCyclingStats Scraper?

No. Error rows, not-found rows and empty results are never billed by ProCyclingStats Scraper. You pay only for race result rows ($0.02 each on the free tier), rider profiles ($0.02), ranking rows ($0.01), startlist riders ($0.005) and team roster riders ($0.005) that were actually delivered, and the run stops cleanly at your maximum charge limit.

#### How many rows can one ProCyclingStats Scraper run return?

Up to 1,000,000 rows on paid Apify plans, set through the `maxItems` field, which caps the total across all modules, so a mixed run of results and rankings shares one limit. Free Apify users are limited to 10 rows per run in ProCyclingStats Scraper.

#### Do I need an API key or login for ProCyclingStats Scraper?

No. ProCyclingStats Scraper reads publicly visible race and rider statistics from procyclingstats.com and does not log in or bypass paywalls, so you do not need a PCS account or API key. You only need an Apify account, which includes the residential proxy.

### 🔗 Want more sports and racing data? Other RecordsData scrapers

- [F1 Racing Results Scraper](https://apify.com/RecordsData/f1-racing-results-scraper): motorsport results since 1950
- [Tennis Match Stats Scraper](https://apify.com/RecordsData/tennis-match-stats-scraper): ATP and WTA stats with rankings
- [Sofascore Match Stats Scraper](https://apify.com/RecordsData/sofascore-match-stats-scraper): live multi-sport stats

### 🆘 Support

Questions or a missing field? Open the Issues tab on the actor page or write to contact.punkrecordsdata@gmail.com.

Last updated: 2026-10-11

# Actor input Schema

## `raceName` (type: `string`):

Race to look up by name, for example Paris-Roubaix, Tour de France, Giro d'Italia or Liège-Bastogne-Liège. Use it with Season (year). If the name does not match a ProCyclingStats race page directly, the actor checks the ProCyclingStats search once. Leave empty when you use URLs, teams, riders or rankings.

## `year` (type: `integer`):

Season of the race, for example 2025. Leave empty for the latest edition that has results.

## `resultScope` (type: `string`):

How much of the race to return. Winner only is one row per race. Final classification is the full finishing order of a one-day race or the final general classification of a stage race. Stage winners is one row per stage. All stages returns the full result of every stage, one page per stage.

## `classification` (type: `string`):

Which final classification to return for a stage race when the scope is Winner only or Final classification: general classification, points, mountains or youth. One-day races ignore it.

## `stage` (type: `integer`):

Return the result of one stage of a stage race, for example 21 for the last stage of the Tour de France. 0 means off (the Result scope decides). One-day races ignore it.

## `raceContent` (type: `string`):

Results returns who finished where. Startlist returns the riders entered in the race with bib number and team, before or after the race. Startlist rows are listed under the startlist record type.

## `raceUrls` (type: `array`):

ProCyclingStats race pages to scrape, one per line. A result page, a stage page, a final classification page or a startlist page works, for example procyclingstats.com/race/tour-de-france/2025/stage-21 or procyclingstats.com/race/paris-roubaix/2025/startlist. A race page without a page name, such as procyclingstats.com/race/paris-roubaix/2025, follows the Result scope above. Team, rider and ranking URLs are also accepted here.

## `teamUrls` (type: `array`):

Teams whose season roster you want, one rider per row with the rider's current PCS rank, career points, age and specialty. Use URLs such as procyclingstats.com/team/uae-team-emirates-xrg-2025, slugs with the season such as uae-team-emirates-xrg-2025, or a team name such as UAE Team Emirates - XRG (the Season field picks the year, default current year).

## `riderUrls` (type: `array`):

Rider profiles to scrape (nationality, birth date, height, weight, specialty points, current and all-time PCS rank), as slugs like tadej-pogacar, full URLs like procyclingstats.com/rider/mathieu-van-der-poel, or plain names like Wout van Aert. A rider that PCS does not know returns one free error row.

## `rankingType` (type: `string`):

Which ProCyclingStats or UCI ranking table to return. Rider tables list 100 riders per page and the actor reads as many pages as Max ranking rows needs. Team and nation tables come whole. Ranking rows are billed as ranking rows.

## `rankingNation` (type: `string`):

Keep only riders of one nation in a rider ranking, with the 2-letter ProCyclingStats nation code, for example be, fr, co or us. Leave empty for all nations.

## `maxRankingRows` (type: `integer`):

How many ranking positions to keep when a ranking table is selected, from 1 to 2500.

## `includeRankings` (type: `boolean`):

Kept for existing integrations: switching it on is the same as choosing the PCS ranking: riders table above. Use Ranking table for the other tables.

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

Maximum number of rows to return across race results, startlists, team riders, rider profiles and ranking rows. Free users are limited to 10 rows (preview); paid users up to 1,000,000.

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

ProCyclingStats sits behind Cloudflare, so Apify RESIDENTIAL proxy is on by default because it is the most reliable. Datacenter proxy also worked in our tests but can be challenged more often.

## Actor input object example

```json
{
  "raceName": "Paris-Roubaix",
  "year": 2025,
  "resultScope": "finalClassification",
  "classification": "gc",
  "stage": 0,
  "raceContent": "results",
  "raceUrls": [],
  "teamUrls": [],
  "riderUrls": [],
  "rankingType": "none",
  "rankingNation": "",
  "maxRankingRows": 100,
  "includeRankings": false,
  "maxItems": 10,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

Key fields per row

## `fullData` (type: `string`):

Complete dataset with all fields

# 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 = {
    "raceName": "Paris-Roubaix",
    "year": 2025,
    "raceUrls": [],
    "teamUrls": [],
    "riderUrls": [],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("recordsdata/procyclingstats-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 = {
    "raceName": "Paris-Roubaix",
    "year": 2025,
    "raceUrls": [],
    "teamUrls": [],
    "riderUrls": [],
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("recordsdata/procyclingstats-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 '{
  "raceName": "Paris-Roubaix",
  "year": 2025,
  "raceUrls": [],
  "teamUrls": [],
  "riderUrls": [],
  "maxItems": 10
}' |
apify call recordsdata/procyclingstats-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,recordsdata/procyclingstats-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/z30qnF9n0EXzaDAo1/builds/hn3ai38MX8IbeoggO/openapi.json
