DEV Community

Cover image for How to Get Route Tolls and Fuel Costs with ViaMichelin Scraper
ReventaData
ReventaData

Posted on Originally published at apify.com

How to Get Route Tolls and Fuel Costs with ViaMichelin Scraper

By the end of this tutorial, you will have a short Python script that turns a trip such as Paris -> Lyon into one row per route alternative: distance, drive time, tolls per gate, vignettes, fuel cost, CO2 and the total.

The Actor does not scrape HTML: it posts to the same JSON route engine the site's pages use. ViaMichelin Scraper (reventadata/viamichelin-scraper) is an Apify Actor that asks that engine directly, with no key or login, and saves each alternative as a dataset row. It is unofficial and not affiliated with ViaMichelin or the Michelin group.

We will configure a route, run the Actor from Python, read its rows and try its options. The sample row below is a sample from a capture dated 2026-09-22, parsed with the Actor's own code.

⏱️ Time estimate: one sitting: install one package, set one token, run three short Python blocks.


Prerequisites

To follow this tutorial, you will need:

  1. Python 3.11+ (apify-client 3.3.0 requires it).
  2. The Apify API client for Python.
  3. An Apify account and its API token, which sits on the API & Integrations page in Apify Console.

Install the client:

pip install "apify-client~=3.0"
Enter fullscreen mode Exit fullscreen mode

The code reads the token from an environment variable:

export APIFY_API_TOKEN="your_apify_token_here"
Enter fullscreen mode Exit fullscreen mode

What you'll achieve

You will turn one or more trips into rows, one per route alternative the engine proposes. Each row carries routeLabel (the engine's summary, such as A6 or N7), routeTypes (labels such as QUICKEST or ECONOMIC), distanceKm, durationMin, tollCost with a tollGates list, vignetteCost with a vignettes list, fuelCost, fuelLitres, co2kg, totalCost and a ViaMichelin result url.

Example use case: Suppose you want to compare the motorway and the toll-free road from Paris to Lyon. In the capture used below, the engine offered two alternatives: the A6 at 463 km, 283.6 minutes and 100.86 EUR in total (41.3 EUR of it tolls), and the N7 at 482 km, 416.8 minutes and 63.0 EUR with no tolls. That is 19 km and 133.2 minutes more for 37.86 EUR less.


Step 1: Access ViaMichelin Scraper

Go to the ViaMichelin Scraper page on Apify Store. Its README lists the main inputs and shows a sample row. In this tutorial we drive the Actor from Python, so everything after this step happens in code.


Step 2: Configure the route

The main input is routes, a list of trips. Each entry can be:

  • "Paris -> Lyon": origin and destination as place names, which ViaMichelin resolves.
  • "Paris -> Dijon -> Lyon": the same with via-stops, in order.
  • "48.85,2.35 -> Lyon": a lat,lng literal skips the place lookup for that stop.
  • An object through the API: {"origin": "Paris", "destination": "Lyon", "waypoints": ["Dijon"]}.

Two inputs apply to every route in the run:

  • mode: CAR, MOTORCYCLE, BIKE or WALK.
  • constraint: NONE, NO_TOLL, NO_MOTORWAY, NO_VIGNETTE, NO_FERRY, NO_BORDER_CROSSING or NO_LOW_EMISSION_ZONE.

maxItems (default 100) caps the rows saved across all routes.

Here is the setup. It prices Paris to Lyon by car with no constraint and saves at most 10 rows, the Actor's prefill input:

import os
from apify_client import ApifyClient

# 1. Initialize ApifyClient with the token from the environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])

# 2. Define the trip: one route, default car, no constraint, at most 10 rows
run_input = {
    "routes": ["Paris -> Lyon"],
    "mode": "CAR",
    "constraint": "NONE",
    "maxItems": 10,
}
Enter fullscreen mode Exit fullscreen mode

💡 Tip: a route string with no separator (->, to, an arrow such as →, or a spaced -) is skipped with an error in the log, and the run continues with the other routes. Hyphens inside a name, as in Aix-en-Provence, are left alone. If no route parses, the run stops at its start.


Step 3: Add optional settings

Parameter Type What it does
currency string Currency code for the cost fields, default eur.
fuelPrice number Energy price per litre (fuel) or per kWh (electric). Omitted, the Actor sends 2.073, the value the site's own request carried on 2026-09-22.
vehicle string A brand and model, such as Tesla Model 3, looked up in the engine's vehicle catalog. Omitted, the site's default car (a Clio V petrol) is used.
initialCharge integer Battery charge at departure, 0 to 100. Sent only when the vehicle is electric; otherwise ignored with a warning.
withCaravan boolean Costs, duration and consumption adapt to towing a caravan.
includeRoadsheet boolean Adds turn-by-turn steps per alternative, at one extra request per route.

Three combinations that work:

  • Toll-free only: constraint: "NO_TOLL". On Paris to Lyon, measured 2026-09-22, it returned the 482 km route with no tolls.
  • Motorcycle tolls: mode: "MOTORCYCLE". Measured the same day, the A6 toll was 24.8 EUR by motorcycle against 41.3 EUR by car.
  • Electric car: vehicle: "Tesla Model 3", initialCharge: 80, fuelPrice: 0.25. Rows then carry electricityKwh, electricityCost, finalCharge and chargeStops.

⚠️ A mode or constraint outside the lists is refused before the run starts. A vehicle the catalog cannot match stops the run at its start.


Step 4: Run the Actor

Now start the run with client.actor("reventadata/viamichelin-scraper").call(run_input=run_input). The call starts the Actor on Apify and waits for it to finish before returning the run:

# 3. Start the Actor run and wait for it to finish
print("Starting ViaMichelin route pricing...")
run = client.actor("reventadata/viamichelin-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
    status = run.status if run else "unknown"
    raise SystemExit(f"Run did not succeed (status: {status}).")

# 4. Fetch the route alternatives
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} route alternatives.\n")
Enter fullscreen mode Exit fullscreen mode

Behind the scenes:

  • The Actor resolves each place name to coordinates through ViaMichelin's own place search, unless you gave lat,lng.
  • It asks the route engine for the trip with your mode, constraint and vehicle, and gets back the alternatives with their costs.
  • It saves one row per alternative, and stops at maxItems.

There is no departure-time setting: the engine has no departure-time parameter, so you cannot price a trip for next Friday's traffic.


Step 5: View and download results

Loop over the rows to print the parts you need:

for item in items:
    print(f"{item['routeLabel']} {item['routeTypes']}")
    print(f"  Distance: {item['distanceKm']} km, {item['durationMin']} min")
    print(f"  Tolls: {item['tollCost']} {item['currency']}")
    for gate in item["tollGates"]:
        print(f"    - {gate['name']}: {gate['amount']} {gate['currency']}")
    print(f"  Fuel: {item['fuelCost']} {item['currency']} ({item['fuelLitres']} L)")
    print(f"  Total: {item['totalCost']} {item['currency']}")
    print("-" * 40)
Enter fullscreen mode Exit fullscreen mode

Here is the first row, a sample from a capture dated 2026-09-22 (Paris to Lyon, car, no constraint) parsed with the Actor's own code. origin, destination and url hold the names ViaMichelin resolves the places to; the capture holds only the route answer, so this sample uses the typed names. A saved row also carries scrapedAt, added at save time:

{
    "id": "450256207d6b-1",
    "origin": "Paris",
    "destination": "Lyon",
    "mode": "CAR",
    "constraint": "NONE",
    "routeLabel": "A6",
    "routeTypes": [
        "SHORTEST",
        "QUICKEST",
        "ECOLOGICAL"
    ],
    "distanceKm": 463,
    "durationMin": 283.6,
    "tollCost": 41.3,
    "fuelCost": 59.56,
    "vignetteCost": 0.0,
    "totalCost": 100.86,
    "currency": "EUR",
    "tollGates": [
        {
            "name": "Villefranche-Limas",
            "amount": 41.3,
            "currency": "EUR"
        }
    ],
    "vignettes": [],
    "fuelLitres": 28.73,
    "co2kg": 66,
    "isEv": false,
    "electricityKwh": null,
    "electricityCost": 0.0,
    "finalCharge": null,
    "chargeStops": [],
    "roadsheet": null,
    "url": "https://www.viamichelin.fr/itineraires/resultats?from=Paris&to=Lyon&travelMode=CAR&tripConstraint=NONE"
}
Enter fullscreen mode Exit fullscreen mode

Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.


Understanding your results

The fields that matter most:

  • routeLabel and routeTypes: the engine's name and labels for the alternative. In the capture, the A6 was SHORTEST, QUICKEST and ECOLOGICAL; the N7 was DISCOVERY and ECONOMIC.
  • distanceKm and durationMin: kilometres, and minutes converted from the engine's milliseconds.
  • tollCost and tollGates: the total and each gate with its amount. The A6 row names one gate, Villefranche-Limas, at 41.3 EUR; the N7 row has an empty list.
  • vignetteCost and vignettes: stickers such as the Swiss vignette. On Lyon to Geneva, measured 2026-09-22, the engine returned the Vignette suisse at 44.5 EUR with its validity message.
  • fuelCost, fuelLitres, co2kg: fuel at the price you set, total litres, CO2 in kilograms.
  • totalCost: the engine's own total, in currency.
  • isEv, electricityKwh, finalCharge, chargeStops: filled for electric vehicles, each stop with lat, lng and atKm.
  • id: a hash of the query plus the alternative's number, so the same input gives the same ids.

Pro tips

  1. Batch trips in one run. Put several entries in routes; they share one mode, constraint and vehicle, and fill one dataset. Run a second time for a different mode.
  2. Set your own fuel price. The 2.073 default dates from 2026-09-22. Pass fuelPrice with today's pump price for fuel costs that match it.
  3. Compare constraints side by side. Run NONE and NO_TOLL on the same routes and join on origin and destination. On a pair where a constraint cannot apply, the result matches NONE.
  4. Turn on the roadsheet only when you need it. includeRoadsheet adds each alternative's steps, with French instructions and mentions such as toll gates and low-emission zones, at one extra request per route.
  5. Use coordinates for exact points. A lat,lng stop skips the place lookup, so a depot or a hotel is routed exactly where you mean, with no guess about which town of that name you meant.

Troubleshooting

  • Run FAILED with zero rows: a run where every route failed or no place could be found ends FAILED. Check the place names in the log.
  • One route missing: a place with no match fails that route only; the log names it, and the run goes on.
  • Run stops at once: no route string had a separator (->, to, an arrow or a spaced -), or the vehicle was not found in the catalog.
  • Fewer rows than expected: read the summary line, Run completed. Pushed N rows. Rejected: {...}; it counts failed routes by error type. From Python: client.log(run.id).get().

For help, use the Actor's Store page.


Pricing and next steps

For current pricing and details, see the Store page.

To reprice routes regularly, save the input as a task and add an Apify schedule, which runs an Actor or task on a cron expression (0 8 * * * is every day at 8 AM). To react when a run finishes, add a webhook on ACTOR.RUN.SUCCEEDED; its payload carries the run object, including defaultDatasetId.

Which trips are you planning to price? Drop a comment! 💬

Top comments (0)