# Komoot Routes & Tours Extractor (`steadfetch/komoot-route-extractor`) Actor

Extract public komoot route records: title, sport, distance, ascent, duration, difficulty, rating and canonical URL. HTML only, sitemap discovery, schema-validated output.

- **URL**: https://apify.com/steadfetch/komoot-route-extractor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** Travel, Sports
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.99 / 1,000 route delivereds

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

## steadfetch/komoot-route-extractor

Extract public komoot route records with [Apify](https://apify.com) pay-per-event pricing and
schema-validated output.

**Data Source: Komoot**

### What it does

The Actor reads public komoot route pages (`/tour/...` and `/smarttour/...`) and saves one validated
dataset item per route:

- route title, sport, distance in kilometres, ascent in metres and ISO 8601 duration;
- published difficulty and aggregate rating (no review text, no reviewer data);
- canonical route URL, route id and page locale.

Routes can be supplied explicitly through `urls`, or discovered from the locale sitemaps declared
in `robots.txt`. The Actor reads server-rendered HTML only: it never calls `/api`, `/webapi`,
`/account`, user sub-pages, or the `/embed`, `/zoom` and `/is-private` variants.

#### Input

```json
{
  "locale": "en",
  "maxResults": 3
}
```

- `locale` (default `en`): sitemap locale, such as `en`, `de-de`, `es-es` or `fr-fr`. Output text
  (title and difficulty) is published in that locale.
- `region` (optional): case-insensitive substring filter on the route title or URL, e.g. `Wales`.
- `urls` (optional): canonical komoot route URLs. When omitted, routes are discovered from the
  locale sitemap. Duplicate route ids are fetched once and billed once.
- `maxResults`: 1 to 10,000 routes saved to the dataset (default 100).

#### Example output

Real output for route `e1005421987` (English page):

```json
{
  "routeId": "e1005421987",
  "title": "Margam Country Park to Brombil Reservoir loop via Heol Y Moch",
  "url": "https://www.komoot.com/smarttour/e1005421987/margam-country-park-to-brombil-reservoir-loop-via-heol-y-moch",
  "sport": "hike",
  "distanceKm": 20.2,
  "ascentM": 520,
  "durationIso": "PT5H51M",
  "difficulty": "Hard",
  "ratingAvg": 3.8,
  "ratingCount": 6,
  "locale": "en",
  "scrapedAt": "2026-10-22T10:20:00.000Z"
}
```

The same route on the German page produces `distanceKm: 9.05` for a different route,
the German difficulty label (the German word for "moderate") and `locale: "de-de"`: JSON-LD property names and decimal separators are
localized, so the extractor classifies values by shape and locale rather than by English key names.
`difficulty` is reported exactly as published, in the page locale.

Every item is validated against `.actor/dataset_schema.json` before it reaches the dataset. Rows
without a published distance are not emitted, and a schema violation fails the run.

### Pricing

Pay-per-event, with the two synthetic Apify events declared in `.actor/pay_per_event.json`:

| Event | Charged when | Price |
| --- | --- | --- |
| `apify-actor-start` | once per run start (synthetic, charged automatically) | 0.00005 USD per run (Apify-recommended default) |
| `apify-default-dataset-item` | once per route saved to the dataset (synthetic, charged automatically) | 0.003 USD per route = **3 USD per 1,000 results** |

That is **3 USD per 1,000 results**, inside the Steadfetch 1-10 USD per 1,000 results band and below
the 3.50 USD/1k incumbent komoot Actor on the Store. There is no custom per-item event and no
`Actor.charge()` call: `apify-default-dataset-item` already bills each result, so a second event
would bill users twice. A run that delivers nothing is only charged the start event.

#### Cost per 1,000 results

Measured locally on 2026-10-22 with the three committed fixtures and Node 24 (no Apify credit):

- **Extraction: 284 ms per page** after a warm-up, about **284 s of CPU per 1,000 routes**.
- **Politeness delay: 1 s per request**, so 1,000 routes need at least **~17 minutes** of wall time.
- At 256 MB, that wall time is **0.071 GB-hour = ~0.014 USD per 1,000 routes** at 0.20 USD/GB-hour
  (compute only). At 512 MB it doubles to ~0.028 USD. Data transfer is not itemised by this local
  measurement and no Apify platform run has been spent yet.
- Expected infrastructure cost is therefore in the **0.01-0.03 USD per 1,000** range, well below
  the 0.10-0.30 USD/1k estimate in the spec. Gross margin at 3.00 USD per 1,000 is above 99%.

When `APIFY_TOKEN` is available, a single minimal platform run with `maxResults: 3` should replace
the estimate above with the platform-measured figure.

### Legal and robots.txt compliance

`robots.txt` (checked 2026-10-22) allows canonical `/tour/...` and `/smarttour/...` pages under
`User-agent: *`; it disallows `/api`, `/webapi`, `/account`, `/terms-of-service`, `/service`,
`/product/buy`, `/tiles`, `/static`, `/plan/tour/`, `/plan/@`, `/discover/...`, user sub-pages and the
embed/zoom/private variants. This Actor:

- builds only canonical route URLs and declared sitemap files, all checked by `assertAllowedUrl()`;
- aborts the run if a redirect leaves that allowlist;
- issues one request at a time with a conservative 1 s delay (robots declares no crawl delay);
- sends an honest `User-Agent: steadfetch-komoot-route-extractor/0.1
  (+https://apify.com/steadfetch; contact: https://apify.com/steadfetch)`;
- collects no personal data: route metadata and aggregate ratings only.

The full evidence is in `docs/legal-gate.md`.

### FAQ

**Does the Actor need a komoot account?**
No. It reads public HTML pages only and never bypasses logins, paywalls or CAPTCHAs.

**How are routes discovered?**
From the locale sitemaps declared in `robots.txt`. You can also pass explicit canonical URLs in
`urls`.

**Why is the sport a canonical slug but the difficulty localized?**
`sport` is normalised to a stable slug (`hike`, `mtb`, `racebike`, ...) so it can be filtered across
locales. `difficulty` is reported exactly as published because komoot's scale labels vary by
language; the `locale` field tells you which language you received.

**Why is `ascentM` sometimes null?**
Only when the page publishes no elevation figure. The Actor never invents one.

**Is the output schema stable?**
Yes. New fields are added additively and required fields do not change. Every item keeps the common
Steadfetch envelope (`url`, `scrapedAt`) plus `routeId`.

**Can I request another source?**
Yes. Open an issue in the Steadfetch Actors repository describing the public page you want
extracted.

### Maintenance

This Actor is **tested daily**: automated runs keep the source reachable and the output schema
valid. Every push runs lint, typecheck, offline fixture tests, the Steadfetch publish gate and
`apify-cli validate-schema` in CI. Schema changes are announced through repository releases.

### Development

```bash
npm install
npm test                # lint + typecheck + offline fixture tests
npm run publish:check   # Steadfetch publish gate
npm run build           # tsc build to dist/
```

Fixtures are real pages saved from komoot on 2026-10-22, gzipped to keep the repository small.
Refetch them (1 s delay, no Apify credit, no token) with
`docs/evidence/fixtures/fetch-fixtures.sh`.

# Actor input Schema

## `locale` (type: `string`):

Locale whose sitemap is used for discovery, such as en, de-de, es-es or fr-fr. Output values are published in that locale.

## `region` (type: `string`):

Optional case-insensitive substring filter on the route title or URL (for example Wales, Denia or Norway). Useful to keep a run small and relevant.

## `urls` (type: `array`):

Optional canonical komoot route URLs (/tour/ or /smarttour/). When omitted, routes are discovered from the locale sitemap. Duplicate route ids are fetched once and billed once.

## `maxResults` (type: `integer`):

Maximum number of routes saved to the dataset. One dataset item is one billable result.

## Actor input object example

```json
{
  "locale": "en",
  "region": "Wales",
  "urls": [
    "https://www.komoot.com/smarttour/e1005421987/margam-country-park-to-brombil-reservoir-loop-via-heol-y-moch"
  ],
  "maxResults": 3
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset items validated against the route item schema (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 = {
    "locale": "en",
    "urls": [
        "https://www.komoot.com/smarttour/e1005421987/margam-country-park-to-brombil-reservoir-loop-via-heol-y-moch"
    ],
    "maxResults": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/komoot-route-extractor").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 = {
    "locale": "en",
    "urls": ["https://www.komoot.com/smarttour/e1005421987/margam-country-park-to-brombil-reservoir-loop-via-heol-y-moch"],
    "maxResults": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/komoot-route-extractor").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 '{
  "locale": "en",
  "urls": [
    "https://www.komoot.com/smarttour/e1005421987/margam-country-park-to-brombil-reservoir-loop-via-heol-y-moch"
  ],
  "maxResults": 3
}' |
apify call steadfetch/komoot-route-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfetch/komoot-route-extractor"
        }
    }
}
```

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/QgagfhkZxWKAL4QAQ/builds/2a5pUx4sySr4vbAJP/openapi.json
