EV Route Planner
Plan a drivable EV route with the charging stops needed to complete it
Endpoint
GET https://api.carapi.dev/v1/ev-routePick a vehicle with vehicleId from /v1/ev-vehicles, or describe one yourself with batteryKwh and consumptionWhKm. Sending both is a 400.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| originLat | number | Required | Origin latitude. Must be inside the European service area. |
| originLon | number | Required | Origin longitude. Must be inside the European service area. |
| destLat | number | Required | Destination latitude. |
| destLon | number | Required | Destination longitude. |
| vehicleId | string | Optional | Catalog vehicle id from /v1/ev-vehicles. Supplies battery, consumption, ports and the model charging curve. Mutually exclusive with the custom parameters below. |
| batteryKwh | number | Optional | Usable battery of a custom vehicle (5-300). Required with consumptionWhKm when vehicleId is omitted. |
| consumptionWhKm | number | Optional | Flat consumption of a custom vehicle in Wh/km (80-400). Required with batteryKwh when vehicleId is omitted. |
| connector | string | Optional | DC connector of a custom vehicle: ccs, chademo or type2. Default ccs. |
| maxDcKw | number | Optional | Peak DC power of a custom vehicle (20-1000). Default 150. |
| initialSoc | number | Optional | State of charge at departure, percent (5-100). Default 90. Must exceed reserveSoc. |
| minArrivalSoc | number | Optional | Minimum state of charge on arrival, percent (0-80). Default 10. |
| reserveSoc | number | Optional | Buffer the planner never drives below, percent (0-50). Default 10. |
| ambientC | number | Optional | Ambient temperature in Celsius (-40 to 50). Default 20. Cold raises consumption and slows charging. |
| maxDetourKm | number | Optional | How far off-route a candidate charger may sit (0.5-10). Default 5. |
| optimize | string | Optional | Optimisation objective. Only 'time' today. |
| includeGeometry | boolean | Optional | Include the encoded polyline of the final route. Off by default because it is large. |
Request Examples
cURL
curl -X GET \
"https://api.carapi.dev/v1/ev-route?originLat=50.087&originLon=14.421&destLat=52.52&destLon=13.405&vehicleId=tesla-model-3-lr-2021&token=YOUR_API_KEY"JavaScript
const params = new URLSearchParams({
originLat: '50.087', // Prague
originLon: '14.421',
destLat: '52.52', // Berlin
destLon: '13.405',
vehicleId: 'tesla-model-3-lr-2021',
initialSoc: '90',
minArrivalSoc: '10',
token: 'YOUR_API_KEY'
})
const res = await fetch(`https://api.carapi.dev/v1/ev-route?${params}`)
const plan = await res.json()
if (!plan.feasible) {
// A valid request that cannot be driven: gap tells you where it breaks
console.log(plan.reason, plan.gap)
} else {
console.log(`${plan.summary.totalMin} min, ${plan.summary.chargeStops} stops`)
for (const stop of plan.stops) {
console.log(`${stop.name}: ${stop.arrivalSocPct}% -> ${stop.departSocPct}% in ${stop.chargeMin} min`)
}
}
// Required by the data licenses — display this wherever you show the plan
console.log(plan.attribution.join(' · '))Python
import requests
resp = requests.get(
"https://api.carapi.dev/v1/ev-route",
params={
"originLat": 50.087,
"originLon": 14.421,
"destLat": 52.52,
"destLon": 13.405,
"vehicleId": "tesla-model-3-lr-2021",
"ambientC": -5, # winter planning: more energy, slower charging
"token": "YOUR_API_KEY",
},
)
plan = resp.json()
print(plan["summary"]["totalMin"], "min")
for stop in plan["stops"]:
print(stop["name"], stop["chargeMin"], "min")Response
Success (200) — Prague to Berlin, one stop
{
"feasible": true,
"summary": {
"distanceKm": 351.2,
"driveMin": 227,
"chargeMin": 19,
"totalMin": 246,
"chargeActiveMin": 12,
"plugOverheadMin": 7,
"offRouteMin": 3,
"offRouteKm": 1.8,
"energyKwh": 64.3,
"arrivalSocPct": 15,
"chargeStops": 1,
"estChargeCostEur": 21.7,
"stopsWithKnownPrice": 1
},
"warnings": [],
"stops": [
{
"name": "EnBW Senftenberger Str. 29, Schipkau",
"owners": ["EnBW"],
"lat": 51.5233,
"lon": 13.9341,
"address": {
"street": "Senftenberger Str. 29",
"city": "Schipkau",
"zip": "01998",
"country": "DE"
},
"maxPowerKw": 300,
"stationCount": 8,
"routeKm": 219,
"arrivalSocPct": 18,
"departSocPct": 62,
"chargeMin": 19,
"kwhAdded": 33.4,
"estCostEur": 21.7,
"detourKm": 1.2,
"alternatives": []
}
],
"legs": [
{ "fromKm": 0, "toKm": 219, "distanceKm": 219, "driveMin": 132, "energyKwh": 38.6 },
{ "fromKm": 219, "toKm": 351.2, "distanceKm": 132.2, "driveMin": 95, "energyKwh": 25.7 }
],
"vehicle": {
"id": "tesla-model-3-lr-2021",
"label": "Tesla Model 3 Long Range Dual Motor 2021",
"usableBatteryKwh": 75,
"consumptionWhPerKm": 160,
"dcMaxKw": 250,
"dcPorts": ["ccs"]
},
"attribution": [
"© OpenStreetMap contributors",
"openrouteservice.org",
"Vehicle data from Open EV Data"
],
"disclaimer": "Estimates only. Consumption and charge times vary with conditions; verify charger status before relying on a stop."
}Not drivable (200)
A valid request whose trip cannot be completed is still a 200 — the answer is “no”, not an error. Check feasible before reading summary, which is null in this case.
{
"feasible": false,
"summary": null,
"warnings": [],
"stops": [],
"legs": [],
"gap": {
"fromKm": 412,
"toKm": 598,
"neededKwh": 31.4
},
"reason": "No charger within range on this stretch",
"attribution": ["© OpenStreetMap contributors", "openrouteservice.org"],
"disclaimer": "Estimates only. Consumption and charge times vary with conditions; verify charger status before relying on a stop."
}Error (400)
{
"error": "Route planning is currently available within Europe only (lat 35..62, lon -11..35)"
}Response Fields
| Field | Type | Description |
|---|---|---|
| feasible | boolean | False when the trip cannot be completed with this vehicle and the chargers we know about. Still a 200. |
| summary.distanceKm | number | Total distance of the route through the chosen stops |
| summary.driveMin | integer | Driving time, excluding time at chargers |
| summary.chargeMin | integer | Total time at chargers, including plug-in overhead |
| summary.totalMin | integer | Trip time: drive + charge |
| summary.chargeActiveMin | integer | Of chargeMin, the part actually delivering energy |
| summary.plugOverheadMin | integer | Of chargeMin, the fixed per-stop cost of pulling in and plugging in |
| summary.offRouteMin | integer | Time spent leaving the direct route to reach chargers |
| summary.energyKwh | number | Total energy consumed over the trip |
| summary.arrivalSocPct | number | State of charge on arrival |
| summary.estChargeCostEur | number|null | Sum over stops with a published price. Null when no stop has one. |
| summary.stopsWithKnownPrice | integer | How many stops the cost estimate is based on — read it alongside estChargeCostEur |
| warnings[] | string | Non-fatal caveats about this plan, e.g. that the route through the chosen chargers is longer than the direct one |
| stops[].routeKm | number | Where along the route this stop sits |
| stops[].arrivalSocPct | number | State of charge on arrival at this charger |
| stops[].departSocPct | number | State of charge when leaving — the planner charges only as much as the next leg needs |
| stops[].chargeMin | integer | Time at this stop, including plug-in overhead |
| stops[].estCostEur | number|null | Null where the operator publishes no price for this connector |
| stops[].alternatives[] | object | Nearby chargers that could substitute for this stop |
| legs[] | object | Per-leg distance, drive time and energy between consecutive stops |
| gap | object | Only when feasible is false: the stretch that cannot be crossed, and the energy it needs |
| attribution[] | string | Required credit for the underlying data — a license condition, not metadata |
Error Codes
Bad Request
Missing coordinate, out-of-range parameter, a route outside Europe, or one longer than 2500 km
Forbidden
Invalid or missing API token
Bad Gateway
Routing or charging data unavailable upstream
Service Unavailable
Routing provider quota exhausted. The budget is shared by all callers and resets daily, so the response carries retryAfterSeconds (and a Retry-After header) — retrying sooner will not succeed. Any other upstream failure is a 502, not this.
Attribution is required
Every response carries an attribution array. Displaying it wherever you present the plan is a condition of the licenses behind the data, not a courtesy:
- •Road network and routing: © OpenStreetMap contributors (ODbL) via openrouteservice.org.
- •Vehicle models and charging curves: Open EV Data.
- •Tesla Supercharger locations: supercharge.info.
Use the array from the response rather than hard-coding this list — it is the authoritative version and will change as sources are added.
Important Notes
- •The planner minimises total trip time, not stop count — it will prefer two short charges on a fast curve over one long one when that gets you there sooner.
- •
feasible: falseis a 200, not an error.summaryis null andgapshows the stretch that cannot be crossed. - •Cost coverage is partial:
estChargeCostEuronly sums the stops whose operator publishes a price. Always read it next tostopsWithKnownPrice— a low count means a low estimate, not a cheap trip. - •
ambientCgenuinely changes the plan. Winter temperatures raise consumption and slow charging, which can add a stop. - •Europe only (lat 35..62, lon -11..35), up to 2500 km per route. Charging data refreshes monthly, so verify a charger is live before relying on it.
- •Each request consumes 8 API credits — more than other endpoints because every uncached plan spends real routing-provider quota. Identical routes are served from cache.