# Cycling Races, Riders & Results (`scrapermaster/cycling-races-riders-results`) Actor

Professional cycling data: race calendar, startlists, stage and GC results, rider profiles with specialty points, and UCI/team/nation rankings. Export to JSON, CSV or Excel.

- **URL**: https://apify.com/scrapermaster/cycling-races-riders-results.md
- **Developed by:** [Scraper Master](https://apify.com/scrapermaster) (community)
- **Categories:** Sports, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.62 / 1,000 results

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

## Cycling Races, Riders & Results

Turns the public pages of ProCyclingStats (procyclingstats.com) into clean, structured rows: the season calendar, who starts a race, who finished where, a rider's profile and specialty scores, and the current or past rankings. No login, no API key, no browser: it reads plain HTML pages, politely.

### What it does

One Actor, five modes. Pick the mode with the `mode` input; every mode writes to the same dataset and charges one `result-item` event per row delivered. Pages that fail produce a separate error row (not charged) instead of stopping the run.

| Mode | You give it | You get |
|---|---|---|
| `calendar` | a year, optionally a circuit and a date window | one row per race |
| `startlist` | one or more races | one row per rider entered |
| `results` | one or more races and a stage | one row per finisher / non-finisher |
| `rider` | rider URLs and/or names | one profile row per rider |
| `rankings` | ranking type and year | one row per ranking place |

### Example inputs

#### Calendar

```json
{ "mode": "calendar", "year": 2026, "circuit": "1", "startDate": "2026-06-01", "endDate": "2026-08-31", "maxItems": 50 }
```

Circuits: `1` UCI WorldTour, `24` Women's WorldTour, `26` ProSeries, `13` Europe Tour, `12` Asia Tour, `11` Africa Tour, `14` Oceania Tour, `18` America Tour, `2` World Championships, `23` National Championships, `16` Women Elite, `15` / `17` Junior. Empty means all circuits. Set `includeStagesCount` to `true` to also count stages (one extra request per race).

#### Startlist

```json
{ "mode": "startlist", "raceUrls": ["race/tour-de-france/2026"], "maxItems": 200 }
```

#### Results

```json
{ "mode": "results", "raceUrls": ["race/tour-de-france/2025"], "stage": "gc", "maxItems": 50 }
```

`stage` can be `gc` (final classification; for one-day races this is the race result), a stage number such as `5`, `prologue`, or `all` for every stage in turn.

#### Rider

```json
{ "mode": "rider", "riderNames": ["Tadej Pogačar"], "riderUrls": ["rider/remco-evenepoel"] }
```

#### Rankings

```json
{ "mode": "rankings", "rankingType": "team", "year": 2025, "maxItems": 30 }
```

`rankingType`: `individual`, `team`, `nations` or `uciIndividual` (current UCI table only). `rankingScope` is `season` (points of the chosen year) or `rolling` (the live rolling ranking, year ignored).

### Output fields

Fields that do not apply to a mode are simply absent. Every row has `mode` and `scrapedAt`.

| Field | Modes | Meaning |
|---|---|---|
| `raceName`, `race` | calendar, startlist, results | Race title |
| `raceUrl`, `raceSlug`, `year` | calendar, startlist, results | Canonical race link, slug and season |
| `startDate`, `endDate` | calendar | ISO dates |
| `country`, `countryCode` | calendar | Host country |
| `category` | calendar | UCI class, for example `2.UWT` |
| `winnerName`, `winnerUrl` | calendar | Winner when the race is finished |
| `stagesCount` | calendar | Only with `includeStagesCount` |
| `bibNumber` | startlist, results | Race number |
| `riderName`, `givenName`, `surname`, `riderUrl`, `riderSlug` | startlist, results | Rider as listed (surname in capitals) |
| `team`, `teamUrl`, `teamClass` | startlist, results, rider, rankings | Team of the race / season |
| `nationality`, `nationalityCode` | startlist, results, rider, rankings | Country name and ISO-style code |
| `statusNote` | startlist | Text such as `DNF #18` when the page shows one |
| `stage`, `stageName` | results | `gc`, `result`, `5`, ... and the title of that stage |
| `rank`, `status` | results | Place (null when not classified) and `FINISHED`, `DNF`, `DNS`, `OTL`, ... |
| `age` | results, rider | Age at the time of the race / today |
| `timeText`, `gapText` | results | Finishing time and gap, as written on the site. Same-time riders repeat the gap above them |
| `points`, `uciPoints` | results | Ranking points awarded (site points and UCI points) |
| `name`, `dateOfBirth`, `placeOfBirth`, `weightKg`, `heightM` | rider | Profile |
| `specialtyPoints` | rider | Object with `oneday`, `gc`, `timeTrial`, `sprint`, `climber`, `hills` |
| `pcsRanking`, `allTimeRanking`, `uciRanking`, `uciPoints`, `careerWins` | rider | Ranking places and win count when shown |
| `profileUrl` | rider | Rider page |
| `rankingType`, `scope`, `previousRank`, `points`, `name`, `url` | rankings | One ranking place |
| `error`, `errorMessage`, `url` | any | Error rows only |

### Good to know

- **Speed and manners.** At most 3 requests in flight (default 2), a short random pause before each, a regular browser User-Agent, and exponential backoff on 429 and 5xx answers. Large `all`-stage runs take a while by design.
- **Costs.** `maxItems` is a hard cap. If the run has a maximum charge, the Actor stops as soon as that budget is spent. Error rows are not billed.
- **Race references.** `race/<slug>/<year>`, the full URL, and the older `race/<slug>-<year>-gc` form copied from calendar links are all accepted. A bare slug needs the `year` input.
- **Team time trials.** The site lists these per team, so every rider of the team gets one row carrying the team's rank, time and gap. Bib, age and points are not available there.
- **Times.** Time trials keep hundredths (`36:42,02`). For non-winners the time is computed from the winner's time plus the listed gap.
- **Rankings by year.** Past seasons use the end-of-year table. Individual tables hold 100 places per page, and the Actor pages through them up to your `maxItems`.
- **Rider names.** A name becomes a profile address by simple transliteration; if a rider is not found, paste the profile URL instead.
- **Not covered.** Women's race pages work only where they follow the same layout; classification jerseys (points, mountains, youth, teams) are not extracted, only the main table.
- Data belongs to its source; use it in line with the source site's terms and applicable law.

### Run locally

```
npm install
mkdir -p storage/key_value_stores/default
echo '{"mode":"calendar","year":2026,"circuit":"1","maxItems":10}' > storage/key_value_stores/default/INPUT.json
CRAWLEE_STORAGE_DIR=./storage npm start
```

Rows land in `storage/datasets/default`.

# Actor input Schema

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

What to extract. calendar: races of a season. startlist: riders entered in a race. results: finishers of a stage / the final classification. rider: rider profiles. rankings: season or rolling rankings.

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

Season used by calendar and rankings. In startlist and results it is only used for race entries given as a bare slug (for example "tour-de-france").

## `circuit` (type: `string`):

Which circuit to list. Leave on 'All circuits' for every race of the year.

## `startDate` (type: `string`):

Optional ISO date (YYYY-MM-DD). Races that finish before this date are skipped.

## `endDate` (type: `string`):

Optional ISO date (YYYY-MM-DD). Races that start after this date are skipped.

## `includeStagesCount` (type: `boolean`):

Opens each race page to count its stages. One extra request per race, so it is slower. Off by default.

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

Race page URLs or short paths, one per line, for example https://www.procyclingstats.com/race/tour-de-france/2025 or race/tour-de-france/2025. URLs copied from the calendar output work as they are.

## `stage` (type: `string`):

gc = final classification (or the result of a one-day race). A number such as 5 = that stage. prologue = the prologue. all = every stage of the race, one after another (one request per stage).

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

Rider page URLs or paths, for example rider/tadej-pogacar.

## `riderNames` (type: `array`):

Plain rider names such as "Jonas Vingegaard". The name is converted to a profile address, so unusual spellings may not be found; use a URL in that case.

## `lookupUciRanking` (type: `boolean`):

Reads up to the first 500 places of the UCI individual ranking (at most five extra requests, shared by all riders) to fill uciRanking and uciPoints.

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

Which ranking table to read.

## `rankingScope` (type: `string`):

season = points scored in the chosen year (end of that year, or today for the current year). rolling = the current rolling ranking; the year is ignored.

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

Hard cap on the number of result rows. The run also stops when your maximum charge for the run is reached.

## `maxConcurrency` (type: `integer`):

Requests in flight at the same time (1 to 3). Small random pauses are added between requests regardless.

## Actor input object example

```json
{
  "mode": "calendar",
  "year": 2026,
  "circuit": "1",
  "includeStagesCount": false,
  "raceUrls": [
    "race/tour-de-france/2025"
  ],
  "stage": "gc",
  "riderUrls": [
    "rider/tadej-pogacar"
  ],
  "riderNames": [],
  "lookupUciRanking": true,
  "rankingType": "individual",
  "rankingScope": "season",
  "maxItems": 20,
  "maxConcurrency": 2
}
```

# Actor output Schema

## `results` (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": "calendar",
    "year": 2026,
    "circuit": "1",
    "raceUrls": [
        "race/tour-de-france/2025"
    ],
    "stage": "gc",
    "riderUrls": [
        "rider/tadej-pogacar"
    ],
    "rankingType": "individual",
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapermaster/cycling-races-riders-results").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": "calendar",
    "year": 2026,
    "circuit": "1",
    "raceUrls": ["race/tour-de-france/2025"],
    "stage": "gc",
    "riderUrls": ["rider/tadej-pogacar"],
    "rankingType": "individual",
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapermaster/cycling-races-riders-results").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": "calendar",
  "year": 2026,
  "circuit": "1",
  "raceUrls": [
    "race/tour-de-france/2025"
  ],
  "stage": "gc",
  "riderUrls": [
    "rider/tadej-pogacar"
  ],
  "rankingType": "individual",
  "maxItems": 20
}' |
apify call scrapermaster/cycling-races-riders-results --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapermaster/cycling-races-riders-results"
        }
    }
}
```

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/6mZfjp5gtJDtc0RvS/builds/9AVELGj7QQoi67j7x/openapi.json
