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:
-
Python 3.11+ (
apify-client3.3.0 requires it). - The Apify API client for Python.
- 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"
The code reads the token from an environment variable:
export APIFY_API_TOKEN="your_apify_token_here"
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": alat,lngliteral 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,BIKEorWALK. -
constraint:NONE,NO_TOLL,NO_MOTORWAY,NO_VIGNETTE,NO_FERRY,NO_BORDER_CROSSINGorNO_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,
}
💡 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 carryelectricityKwh,electricityCost,finalChargeandchargeStops.
⚠️ A
modeorconstraintoutside the lists is refused before the run starts. Avehiclethe 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")
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)
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"
}
Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
routeLabelandrouteTypes: the engine's name and labels for the alternative. In the capture, the A6 wasSHORTEST,QUICKESTandECOLOGICAL; the N7 wasDISCOVERYandECONOMIC. -
distanceKmanddurationMin: kilometres, and minutes converted from the engine's milliseconds. -
tollCostandtollGates: 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. -
vignetteCostandvignettes: stickers such as the Swiss vignette. On Lyon to Geneva, measured 2026-09-22, the engine returned theVignette suisseat 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, incurrency. -
isEv,electricityKwh,finalCharge,chargeStops: filled for electric vehicles, each stop withlat,lngandatKm. -
id: a hash of the query plus the alternative's number, so the same input gives the same ids.
Pro tips
-
Batch trips in one run. Put several entries in
routes; they share onemode,constraintand vehicle, and fill one dataset. Run a second time for a different mode. -
Set your own fuel price. The 2.073 default dates from 2026-09-22. Pass
fuelPricewith today's pump price for fuel costs that match it. -
Compare constraints side by side. Run
NONEandNO_TOLLon the same routes and join onoriginanddestination. On a pair where a constraint cannot apply, the result matchesNONE. -
Turn on the roadsheet only when you need it.
includeRoadsheetadds each alternative's steps, with French instructions and mentions such as toll gates and low-emission zones, at one extra request per route. -
Use coordinates for exact points. A
lat,lngstop 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 thevehiclewas 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)