By the end of this tutorial, you will have a short Python script that turns a map rectangle into a list of EV charging stations from Electromaps: name, address, coordinates, connector mix, highest power in kW, community rating, opening hours and the price text the station publishes.
Electromaps is a charging-station map. It has no self-serve public API, so getting its stations into a spreadsheet by hand means clicking through the map one pin at a time.
In this guide, we use Python and the Apify Actor Electromaps EV Charging Scraper (reventadata/electromaps-scraper). You will draw a bounding box, add filters, run the Actor and read the rows. The sample row shown 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 a bounding box into one row per charging station. Each row carries the station id and url, name, address, city, postalCode, countryCode, latitude and longitude, the site type (locationType), rating and ratingCount, openTime, chargePrice, and a connectors list with each connector's type, status, power, voltage and amperage.
Note: statuses come back exactly as Electromaps reports them. Where a station's
realtimeflag isfalse, its statuses are crowdsourced, not live, andUNKNOWNstaysUNKNOWN.
Example use case: Suppose you want every on-street or parking-lot charger of at least 50 kW inside a city. You would set the four box edges around the city, locationTypes: ["ON_STREET", "PARKING"] and minPowerKw: 50, and get one row per matching station with its connectors listed.
Step 1: Access Electromaps EV Charging Scraper
Go to the Electromaps 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 four required inputs are the edges of the search box, in WGS84 decimal degrees (the latitude and longitude numbers any map shows):
-
bboxWestandbboxEast: the minimum and maximum longitude, between -180 and 180. -
bboxSouthandbboxNorth: the minimum and maximum latitude, between -90 and 90.
West must be smaller than east and south smaller than north; otherwise the run stops with an error at its start. A value outside the ranges above is refused before the run starts.
Here is the setup. It uses the Actor's prefill box, a small rectangle in Vilanova i la Geltrú, Spain, with on-street and parking sites, detail on, 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 search: one small box, two site types, at most 10 stations
run_input = {
"bboxWest": 1.7227,
"bboxSouth": 41.2135,
"bboxEast": 1.724,
"bboxNorth": 41.2145,
"locationTypes": ["ON_STREET", "PARKING"],
"maxItems": 10,
"fetchDetails": True,
}
💡 Tip: on a web map that displays coordinates, read the four numbers off two opposite corners of the area you want. Start with a small box; you can widen it once the rows look right.
Step 3: Add optional filters
The first four are sent to Electromaps with the box, so stations that do not match never come back; the last two are the Actor's own settings:
| Parameter | Type | What it does |
|---|---|---|
locationTypes |
array | Site types to keep. Omit it for all 12 the Actor knows. |
minPowerKw |
integer | Minimum connector power in kW. 0 turns it off. |
connectors |
array | Connector codes such as IEC_62196_T2 or CHADEMO. Empty turns it off. |
realtimeOnly |
boolean | Only stations that report live data. |
maxItems |
integer | Cap on stations processed (default 100). |
fetchDetails |
boolean | Not a filter: one extra request per station for connectors, ratings and prices (default on). |
The 12 site types the Actor accepts are ON_STREET, PARKING, AIRPORT, CAMPING, HOTEL, RESTAURANT, SHOP, WORKSHOP, FUEL_STATION, CAR_DEALER, MALL and TAXI. Lowercase is fine; the Actor upper-cases them.
Three combinations that work:
-
Fast chargers in a city: a city box,
minPowerKw: 50. -
Hotel chargers for a trip: a region box,
locationTypes: ["HOTEL", "CAMPING"]. -
A quick pin map: any box,
fetchDetails: falsefor id, name and coordinates only.
⚠️ A site type outside those 12 stops the run at its start, before any request is sent: Electromaps answers unknown types with HTTP 403.
Step 4: Run the Actor
Now start the run with client.actor("reventadata/electromaps-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 Electromaps extraction...")
run = client.actor("reventadata/electromaps-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 sends one request for the stations inside your box, with your filters, to the JSON that Electromaps' own map reads. No HTML is scraped, and no key or login is involved.
- With
fetchDetailson, it then requests each station's detail, up tomaxItemsstations, and turns it into one row. - A station whose detail fails is logged and skipped; the run carries on with the rest.
Step 5: View and download results
Loop over the rows to print the parts you need:
for item in items:
print(item["name"], "|", item.get("city"))
print(f" Max power: {item.get('maxPowerKw')} kW, connectors: {item.get('connectorCount')}")
print(f" Status: {item.get('offlineStatus')} (realtime: {item.get('realtime')})")
print(f" Price: {item.get('chargePrice') or 'not published'}")
for conn in item.get("connectors") or []:
print(f" {conn['type']} {conn['powerKw']} kW {conn['powerType']}: {conn['status']}")
print("-" * 40)
Here is one row, a sample from a capture dated 2026-09-22 (the station inside the prefill box) parsed with the Actor's own code. A saved row also carries scrapedAt, the run's timestamp:
{
"id": 307432,
"name": "Ribes Roges 11 Vilanova i la Geltrú",
"url": "https://map.electromaps.com/en/p/307432",
"address": "Passeig de Ribes Roges, 10, 08800, Vilanova i la Geltrú",
"city": "Vilanova i la Geltrú",
"postalCode": "08800",
"countryCode": "ES",
"latitude": 41.213871159682,
"longitude": 1.723192892969,
"locationType": "ON_STREET",
"rating": 3.1481,
"ratingCount": 27,
"onlineStatus": "UNKNOWN",
"offlineStatus": "WORKING",
"realtime": false,
"openTime": "24h",
"chargePrice": "Gratuito",
"parkingPrice": "",
"priceKwhFrom": null,
"instructions": "App EVCharge",
"contact": "Etecnic",
"reservable": false,
"onlyTesla": false,
"connectorCount": 2,
"maxPowerKw": 22.0,
"connectors": [
{
"type": "IEC_62196_T2",
"status": "UNKNOWN",
"powerKw": 22.0,
"powerType": "AC_3_PHASE",
"voltage": 400,
"amperage": 32,
"format": "SOCKET"
},
{
"type": "IEC_62196_T2",
"status": "WORKING",
"powerKw": 22.0,
"powerType": "AC_3_PHASE",
"voltage": 400,
"amperage": 32,
"format": "SOCKET"
}
]
}
Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
idandurl: Electromaps' numeric station id and the station's page on its map (map.electromaps.com/en/p/<id>), handy as a join key between runs. -
maxPowerKwandconnectorCount: the highest connector power and the number of connectors. In the sample, two 22 kW connectors. -
connectors: one object per connector.typeis Electromaps' code (IEC_62196_T2here),powerTypeis the current type (AC_3_PHASEhere), plusvoltage,amperageandformat(SOCKET). -
status,offlineStatus,onlineStatus,realtime: verbatim. The sample hasrealtime: false, so its statuses are crowdsourced; one connector readsWORKINGand the otherUNKNOWN, and the Actor guesses neither. -
chargePriceandparkingPrice: text as the station publishes it.Gratuitois Spanish for free; the sample'sparkingPriceis an empty string.priceKwhFromis a number only when a per-kWh price is published. -
ratingandratingCount: the community rating, 3.1481 from 27 ratings in the sample. -
instructionsandcontact: access notes (App EVCharge) and the operator's name (Etecnic). Uploader usernames, avatars and ads are dropped.
Pro tips
-
Size the box to
maxItems. A Barcelona-area box in the 2026-09-22 capture held 1,955 stations, andmaxItemsdefaults to 100. Raise the cap or split a city into several boxes. -
Filter on the server.
locationTypes,minPowerKw,connectorsandrealtimeOnlyare applied by Electromaps, so a narrow filter shrinks the list before any detail request. -
Skip details for a pin map.
fetchDetails: falsesaves id, name and coordinates only, with no extra request per station. -
Treat statuses as reported, not live. Check
realtimebefore you show a connector as available. For live-only data, setrealtimeOnly: true. -
Start small. Keep
maxItems: 10until your downstream code handles the fields, then raise it. -
Connect it to the rest of your pipeline. A webhook on
ACTOR.RUN.SUCCEEDEDhands your endpoint the run object with itsdefaultDatasetId, so a script can load each new dataset as soon as a run ends. For bigger datasets,iterate_items()pages through the rows instead of loading them in one call.
Troubleshooting
-
Zero rows: a run that is blocked or cannot read a station's detail ends FAILED; an empty box or an over-strict filter ends SUCCEEDED with an empty dataset and a status message saying so. Widen the box or loosen
minPowerKw. -
Run stops at once: west is not smaller than east, south is not smaller than north, or a
locationTypesvalue is not one of the 12. - Input refused before the run starts: a box edge outside -180 to 180 or -90 to 90, or a required edge missing.
-
Fewer rows than expected: read the summary line near the end of the log,
Run completed. Pushed N rows. Rejected: {...}; it counts skipped stations by reason, such as a failed detail request. 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 refresh a box 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 area are you planning to map? Drop a comment! 💬
Top comments (0)