---
title: "Plans an EV journey with its charge stops (see module docs)."
method: POST
path: "/v1/ev/plan"
tags: ["routing"]
---

# Plans an EV journey with its charge stops (see module docs).

`POST /v1/ev/plan`

## Request

- Base URL: `https://api.mapmap.ai`
- URL: `https://api.mapmap.ai/v1/ev/plan`
- Auth: one of:
  - HTTP bearer
  - API key in query parameter `api_key`

# The algorithm

1. **Base route.** One `auto` route over origin + waypoints +
   destination, with the request's `costing_options` verbatim.
2. **Leg sampling.** Each manoeuvre of that route becomes one
   [`sn_ev::LegSample`]: its length and its own average speed, which is
   finer than a leg summary and so makes the steady-state assumption in
   [`sn_ev::consumption_over_legs`] better. Manoeuvre-free routes fall
   back to one sample per leg, and a route with more than
   [`MAX_LEG_SAMPLES`] manoeuvres has consecutive ones coalesced.
3. **Elevation, or an honest absence.** When elevation is enabled the
   gateway's own height service is asked for the elevation at every leg
   boundary, and each leg's gain and loss come from its two ends. When
   it is not enabled, or the DEM has no coverage, the legs are built
   with [`sn_ev::LegSample::without_gradient`], the consumption model
   runs flat (the only thing it can do) and the response says
   `gradient_data: "absent"`. A gradient is **never** silently assumed
   to be zero.
4. **Direct plan.** [`sn_ev::consumption_over_legs`] over the whole
   route from `start_soc`. If the arrival state of charge is at or above
   `min_arrival_soc` and the reserve was never breached, that is the
   plan: no charger work and no matrix call happen at all.
5. **Candidates.** Otherwise, charge points from the store are projected
   onto the route ([`corridor::nearest_on_route`]), filtered to those
   the vehicle can plug into at a rated power inside the corridor
   budget, and capped at [`MAX_PLAN_CANDIDATES`] spread along the route
   rather than clustered; the `/v1/charging/along` corridor machinery,
   with a spread instead of a power ranking, because a planner needs
   reach and a search needs kilowatts.
6. **One matrix.** Origin + candidates → candidates + destination, one
   `(n+1)²` matrix through the gateway's own engine with the request's
   costing options, giving every hop a real driving time and distance:
   never a crow-flies guess.
7. **Greedy with lookahead.** [`crate::ev::plan_stops`] walks the route
   choosing stops so the state of charge never breaches `reserve_soc`,
   preferring a stop that finishes the journey, then the furthest one
   (fewer stops), then the cheapest by driving plus charging time. A
   charger from which nothing further can be reached even on a full
   battery is refused rather than taken. At most eight passes.
8. **Final geometry.** The chosen stops are merged with the waypoints in
   route order and the route is recomputed through them, so the geometry
   returned is the journey actually described.
9. **Or an honest no.** When no plan exists (a charger desert, a
   connector mismatch, a gap wider than the vehicle's range), the answer
   is a 200 with `feasible: false`, the named cause, and the furthest
   point on the route the vehicle can actually reach. Never a fabricated
   plan.

# Stated limitations

A hop's energy is sliced out of the base route's legs and the detour
surplus charged as one gradient-unknown leg (see
[`crate::ev::hop_legs`]); matrix cells are upper bounds on route cost,
not route costs; the consumption model's own ±15 % motorway bound and
its urban bias are stated in [`sn_ev`]; charging curves are best-case,
measured on a warm pack. The plan is a plan, not a promise.

## Request body

- object

## Response `200`

The plan, feasible or honestly not: `{feasible, plan: {stops, total_drive_s, total_charge_s, total_duration_s, total_distance_m, start_soc, arrival_soc, min_arrival_soc, reserve_soc, energy_kwh}, stops[]: {charger_id, name, operator, source, lat, lon, connector: {standard, power_kw, dc, power_kw_source; `derived` when the feed did not publish the rating}, charger_kw, arrive_soc, depart_soc, charge_s, detour_s, along_route_position, off_route_m, status, status_live}, legs[]: {from, to, duration_s, distance_m, start_soc, end_soc, min_soc, consumed_wh, regen_wh}, soc_trace[], gradient_data: "complete" | "partial" | "absent", gradient: {legs_total, legs_with_gradient, fraction}, route: {duration_s, distance_m, length_m}, geometry_polyline6, coverage_note (ALWAYS present), sources[], availability: {live, note}, provenance: {profile, availability, consumption_model, charge_model}, charging_attribution}`. When `feasible` is false the answer carries `reason: {code, message}` and `furthest_reachable`, never a fabricated stop list

- object

## Other responses

- `400`: Invalid request
- `401`: Missing or invalid API key
- `429`: Quota or rate limit exceeded
- `501`: The charge-point dataset is not enabled (SN_CHARGE_DIR unset)
- `502`: Upstream routing engine failed

---

[API](https://skmtc.dev/mapmap/apis/sn-gateway.md) · [All operations](https://skmtc.dev/mapmap/apis/sn-gateway/llms.txt) · [OpenAPI document](https://skmtc.dev/mapmap/apis/sn-gateway/revisions/2293be8b4c38?raw)
