By the end of this tutorial, you will have a short Python script that turns a rectangle on the map into one row per EV charging station: network, coordinates, top power in kW, each connector group, and how many connectors are available, charging or out of order.
The data comes from A Better Routeplanner (ABRP), an EV route planner whose web app loads chargers for the visible map area from its backend API. In this guide, we use Python and the Apify Actor ABRP EV Charging Scraper (reventadata/abrp-scraper), an unofficial tool not affiliated with ABRP or Iternio Planning AB. You will set a bounding box, run the Actor and filter by power and connector. The sample row below is a sample from a capture dated 2026-10-12, parsed with the Actor's own parser.
⏱️ 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 a map rectangle into one row per charging station. Each row carries the station id, networkName, latitude and longitude, an operational status, maxPowerKw, connector totals (totalConnectors, availableConnectors, chargingConnectors, outOfOrderConnectors), a connectors list with one entry per group, and a sourceUrl that opens the station on ABRP.
Note: ABRP reports power in watts; the Actor converts it to kW, rounded to one decimal.
Example use case: Suppose you want CCS chargers of at least 150 kW in one city. You would set the four box edges around the city, minPowerKw: 150 and connectors: ["CCS"], and every saved station would have at least one CCS group and a top power of 150 kW or more.
Step 1: Access ABRP EV Charging Scraper
Go to the ABRP EV Charging Scraper page on Apify Store. The README lists every input and output field. In this tutorial we drive the Actor from Python, so everything after this step happens in code.
Step 2: Configure the bounding box
The main input is the box: four numbers in WGS84 decimal degrees, the format GPS and most map tools use.
-
bboxWestandbboxEast: the left and right longitudes (-180 to 180). -
bboxSouthandbboxNorth: the bottom and top latitudes (-90 to 90).
All four are required. maxItems (default 100) caps the stations saved.
Here is the setup. It uses the input schema's prefill box, which contains the centre of Madrid (40.4169, -3.7033), and saves at most 10 stations:
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 area: the input schema's prefill box, at most 10 stations
run_input = {
"bboxWest": -3.75,
"bboxSouth": 40.40,
"bboxEast": -3.65,
"bboxNorth": 40.45,
"maxItems": 10,
}
💡 Tip: west must be smaller than east and south smaller than north, or the run stops with an error at its start. A box that crosses the 180th meridian therefore has to be split into two runs.
No proxy setting is needed: the proxy is off by default, and in recon on 2026-10-12 direct HTTPS calls reached ABRP's API host with no proxy, browser or challenge.
Step 3: Add optional filters
Two filters run on each station after the Actor reads it. Stations they drop never reach your dataset:
| Parameter | Type | What it keeps |
|---|---|---|
minPowerKw |
integer | Stations whose fastest connector reaches N kW. 0 (the default) turns it off. |
connectors |
array | Stations with at least one of the listed standards: CCS, TYPE2, CHADEMO. Case does not matter; Type 2 with a space does not match. |
apiKey |
secret string | Not a filter: your own Iternio API key. When it is left empty, the Actor uses the key ABRP's web client sends. |
Three combinations that work as input:
-
Motorway-speed CCS:
minPowerKw: 150,connectors: ["CCS"]. -
CHAdeMO drivers:
connectors: ["CHADEMO"], no power floor. -
Everything in a box: no filters,
maxItems: 100.
⚠️ A coordinate outside its range, or a missing box edge, is refused before the run starts.
Step 4: Run the Actor
Now start the run with client.actor("reventadata/abrp-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 ABRP charger extraction...")
run = client.actor("reventadata/abrp-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 saved stations
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} stations.\n")
Behind the scenes:
- The Actor asks ABRP's bounding-box search for charger IDs inside your box, up to four times
maxItems(at least 100). - It fetches the full station records in batches of 50, maps each to a row and applies your filters.
- It stops once
maxItemsstations are saved or the IDs run out.
Step 5: View and download results
Loop over the rows to print the parts you need:
for item in items:
print(f"{item.get('networkName') or 'unknown network'} ({item['id']})")
print(f" Max power: {item['maxPowerKw']} kW")
print(f" Free now: {item['availableConnectors']} of {item['totalConnectors']}")
for c in item["connectors"]:
print(f" {c['standard']} {c['powerKw']} kW: {c['available']} of {c['count']} free")
print(f" Map: {item['sourceUrl']}")
print("-" * 40)
Here is one row, a sample from a capture dated 2026-10-12 (one station record of the Eviny network, not from the Madrid box above) parsed with the Actor's own parser:
{
"id": "1986998632",
"name": "Eviny",
"networkId": 12498,
"networkName": "Eviny",
"latitude": 55.717689514160156,
"longitude": 13.165938377380371,
"status": "OPEN",
"hasDynamicStatus": true,
"maxPowerKw": 350.0,
"totalConnectors": 23,
"availableConnectors": 18,
"chargingConnectors": 2,
"outOfOrderConnectors": 3,
"connectors": [
{
"standard": "CCS",
"powerKw": 350.0,
"count": 10,
"available": 7,
"charging": 2,
"outOfOrder": 1
},
{
"standard": "CCS",
"powerKw": 200.0,
"count": 6,
"available": 4,
"charging": 0,
"outOfOrder": 2
},
{
"standard": "CHADEMO",
"powerKw": 63.0,
"count": 6,
"available": 6,
"charging": 0,
"outOfOrder": 0
},
{
"standard": "TYPE2",
"powerKw": 22.0,
"count": 1,
"available": 1,
"charging": 0,
"outOfOrder": 0
}
],
"sourceUrl": "https://abetterrouteplanner.com/?charger_id=1986998632"
}
Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
maxPowerKw: the highest power of any connector group. In the sample it is 350.0, from the first CCS group. -
connectors: one entry per group, withstandard,powerKw,countand the status split. The sample has four groups: CCS at 350 and at 200 kW, CHAdeMO at 63 kW and Type 2 at 22 kW. -
availableConnectors,chargingConnectors,outOfOrderConnectors: totals across groups, as ABRP reported them when the record was fetched. In the sample, 18 of 23 were available, 2 charging and 3 out of order. -
outOfOrdercounts both statuses ABRP uses for a broken plug,OUTOFORDERandINOPERATIVE: the sample's 200 kW group had one of each, so it shows 2. -
totalConnectorssums every group's count, so a status the Actor does not bucket still counts there. -
hasDynamicStatus: whether live connector status is available for the station. -
namecurrently holds the network name, the same value asnetworkName.
Pro tips
- Draw a small box first. The prefill box covers a slice of one city. Start there, check the fields, then widen it.
-
Raise
maxItemswhen you filter. The Actor requests four timesmaxItemscandidate IDs (at least 100) and filters after fetching, so a strict filter can leave fewer thanmaxItemsstations even when the area has more. -
Know what
minPowerKwtests. It compares the station's fastest connector. A 350 kW station passesminPowerKw: 150and still lists its 22 kW group inconnectors; filter the list yourself if you need only the fast plugs. -
Use the standard names as written.
CCS,TYPE2andCHADEMOare the values in the sample; lowercase works, extra spaces do not. -
Split big areas. Several smaller boxes, run as separate runs or scheduled tasks, keep each run inside its
maxItems. -
Automate from code. The same
run_inputworks with.start()when you do not want to wait, and with a webhook that loads each finished dataset.
Troubleshooting
- Run FAILED with zero rows: the Actor fails a run that saves nothing when it cannot confirm the box really was empty: the search returned no charger IDs, or a batch of station details came back empty. A box where stations loaded normally but every one failed your filters ends SUCCEEDED with an empty dataset and a status message listing the rejections.
-
Run stops at its start:
bboxWestwas not smaller thanbboxEast, orbboxSouthnot smaller thanbboxNorth. - Input refused before the run starts: a box edge is missing or out of range.
-
Fewer stations than expected: read the log.
Discovered N charger ID(s) across bounding box.gives the candidates;Sweep complete: N stations emitted, rejections: ...counts the filter drops. 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 watch one area, 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 city or corridor are you planning to map chargers for? Drop a comment! 💬
Top comments (0)