EV Route Planner

Plan a drivable EV route with the charging stops needed to complete it

GET Request
~2s uncached, ~100ms cached
1 credit per request

Endpoint

GET https://api.carapi.dev/v1/ev-route

Pick a vehicle with vehicleId from /v1/ev-vehicles, or describe one yourself with batteryKwh and consumptionWhKm. Sending both is a 400.

Query Parameters

ParameterTypeRequiredDescription
originLatnumberRequiredOrigin latitude. Must be inside the European service area.
originLonnumberRequiredOrigin longitude. Must be inside the European service area.
destLatnumberRequiredDestination latitude.
destLonnumberRequiredDestination longitude.
vehicleIdstringOptionalCatalog vehicle id from /v1/ev-vehicles. Supplies battery, consumption, ports and the model charging curve. Mutually exclusive with the custom parameters below.
batteryKwhnumberOptionalUsable battery of a custom vehicle (5-300). Required with consumptionWhKm when vehicleId is omitted.
consumptionWhKmnumberOptionalFlat consumption of a custom vehicle in Wh/km (80-400). Required with batteryKwh when vehicleId is omitted.
connectorstringOptionalDC connector of a custom vehicle: ccs, chademo or type2. Default ccs.
maxDcKwnumberOptionalPeak DC power of a custom vehicle (20-1000). Default 150.
initialSocnumberOptionalState of charge at departure, percent (5-100). Default 90. Must exceed reserveSoc.
minArrivalSocnumberOptionalMinimum state of charge on arrival, percent (0-80). Default 10.
reserveSocnumberOptionalBuffer the planner never drives below, percent (0-50). Default 10.
ambientCnumberOptionalAmbient temperature in Celsius (-40 to 50). Default 20. Cold raises consumption and slows charging.
maxDetourKmnumberOptionalHow far off-route a candidate charger may sit (0.5-10). Default 5.
optimizestringOptionalOptimisation objective. Only 'time' today.
includeGeometrybooleanOptionalInclude 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

FieldTypeDescription
feasiblebooleanFalse when the trip cannot be completed with this vehicle and the chargers we know about. Still a 200.
summary.distanceKmnumberTotal distance of the route through the chosen stops
summary.driveMinintegerDriving time, excluding time at chargers
summary.chargeMinintegerTotal time at chargers, including plug-in overhead
summary.totalMinintegerTrip time: drive + charge
summary.chargeActiveMinintegerOf chargeMin, the part actually delivering energy
summary.plugOverheadMinintegerOf chargeMin, the fixed per-stop cost of pulling in and plugging in
summary.offRouteMinintegerTime spent leaving the direct route to reach chargers
summary.energyKwhnumberTotal energy consumed over the trip
summary.arrivalSocPctnumberState of charge on arrival
summary.estChargeCostEurnumber|nullSum over stops with a published price. Null when no stop has one.
summary.stopsWithKnownPriceintegerHow many stops the cost estimate is based on — read it alongside estChargeCostEur
warnings[]stringNon-fatal caveats about this plan, e.g. that the route through the chosen chargers is longer than the direct one
stops[].routeKmnumberWhere along the route this stop sits
stops[].arrivalSocPctnumberState of charge on arrival at this charger
stops[].departSocPctnumberState of charge when leaving — the planner charges only as much as the next leg needs
stops[].chargeMinintegerTime at this stop, including plug-in overhead
stops[].estCostEurnumber|nullNull where the operator publishes no price for this connector
stops[].alternatives[]objectNearby chargers that could substitute for this stop
legs[]objectPer-leg distance, drive time and energy between consecutive stops
gapobjectOnly when feasible is false: the stretch that cannot be crossed, and the energy it needs
attribution[]stringRequired credit for the underlying data — a license condition, not metadata

Error Codes

400

Bad Request

Missing coordinate, out-of-range parameter, a route outside Europe, or one longer than 2500 km

403

Forbidden

Invalid or missing API token

502

Bad Gateway

Routing or charging data unavailable upstream

503

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: false is a 200, not an error. summary is null and gap shows the stretch that cannot be crossed.
  • Cost coverage is partial: estChargeCostEur only sums the stops whose operator publishes a price. Always read it next to stopsWithKnownPrice — a low count means a low estimate, not a cheap trip.
  • ambientC genuinely 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.