Skip to content
Documentation menu
docs / api / post-v1-errands-plan
Routing

POST/v1/errands/plan

Access and price

AuthAn API key, as Authorization: Bearer snk_… or ?api_key=snk_…. Metered. Needs a full key; a browser (maps) key is refused.
PriceA flat 10 calls. Class: standard, or premium for truck and ADR work. Published as unit_rules.errand_plan in /pricing.json.
PremiumWhen the body asks for truck routing: "costing": "truck", a top-level adr object or "hazmat": true

What it does

Returns 200 whether or not the chain fits: an infeasible chain is an answer, with the errand that costs the most named and the largest set that does fit planned out in full. The precedent is POST /v1/ev/plan, which answers feasible: false with a named cause rather than inventing a journey, and the reasoning is identical. A driver who cannot do all three things needs to be told which two they can.

Call it

bash
curl -fsS -X POST "https://api.mapmap.ai/v1/errands/plan" \
  -H "Authorization: Bearer $MAPMAP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"arrive_by":"string","destination":{"lat":0,"lon":0},"errands":[{"category":"string","dwell_minutes":0,"id":"string"}],"origin":{"lat":0,"lon":0}}'

Request body

application/json · ErrandPlanRequest

FieldTypeRequiredDescription
arrive_bystringyesThe hard arrival time, RFC 3339 with an offset.
costing_optionsnot statednoValhalla costing options, passed to every engine call. The costing itself is always auto.
depart_atstringnoWhen they set off, RFC 3339. Defaults to now, in the offset arrive_by carries.
destinationDestinationyesWhere they have to be, and what to call it.
errandsErrandInput[]yesThe errands, in any order: the answer decides the order.
max_detour_minutesnumbernoHow far off the direct route a candidate may sit, as a detour in minutes. Default [DEFAULT_MAX_DETOUR_MINUTES].
originPointyesWhere the driver sets off from.

Example: A request assembled from this schema's own field examples, enums, defaults and types; not a recorded call

json
{
  "arrive_by": "string",
  "destination": {
    "lat": 0,
    "lon": 0
  },
  "errands": [
    {
      "category": "string",
      "dwell_minutes": 0,
      "id": "string"
    }
  ],
  "origin": {
    "lat": 0,
    "lon": 0
  }
}

Responses

200The plan, feasible or honestly not: {feasible, departure, arrive_by, arrival, slack_s, plan: {stops[]: {errand, kind, category, place: {name, label, lat, lon, id}, arrive, depart, dwell_s, drive_s, distance_m, hours: {verdict, tag, source, message}}, final_leg, total_drive_s, total_dwell_s, total_duration_s, total_distance_m}, errands[], search, opening_hours, geometry_polyline6, usage, verification, attribution}. geometry_note replaces geometry_polyline6 when the follow-up route through the stops failed: the plan stands, only the drawn line is missing. hours_note appears when the window crosses the hour a clock change can fall on and a stop was reached after it: those stops read unknown whatever their tag says. When feasible is false the answer carries reason {code, message, over_by_s, blocking: {errand, adds_s}} and, where anything at all fits, achievable: the largest set of errands that does fit, planned in full, with dropped naming what had to go
FieldTypeRequiredDescription
arrivalstringnoPlanned arrival, ISO 8601.
arrive_bystringnoThe deadline as sent.
attributionstringnoPlace data attribution.
costingstringnoThe costing used.
departurestringnoPlanned departure, ISO 8601.
errandsobject[]noEach errand: its category, the place chosen, the dwell and how many candidates were costed.
eta_calibrationobjectnoThe typical-delay calibration applied to the drive times.
feasiblebooleannoWhether every errand fits before arrive_by.
geometry_polyline6stringnoThe whole trip shape, precision-6 encoded polyline.
opening_hoursobjectnoHow opening hours were applied to candidates, and how many carried them.
planobjectnoThe chosen stops in order (place, arrive, depart, dwell_s, drive_s), the final_leg and the totals.
searchobjectnoThe candidate search: radius, corridor and counts.
slack_snumbernoSeconds to spare before the deadline.
usagestringnoWhen the plan is meant to be made (before setting off).
verificationstringnoWhat every stop is: a place in this deployment's index, or a coordinate the caller supplied.

Example: Recorded from the hosted gateway on 2026-11-05 with a test key: POST /v1/errands/plan, a pharmacy and a petrol station on a Brighton school run; lists trimmed to their first items

json
{
  "arrival": "2026-11-05T21:14:53Z",
  "arrive_by": "2026-11-06T15:15:00Z",
  "attribution": "Place data © OpenStreetMap contributors, ODbL.",
  "costing": "auto",
  "departure": "2026-11-05T20:42:13Z",
  "errands": [
    {
      "candidates_costed": 6,
      "candidates_in_corridor": 25,
      "category": "pharmacy",
      "dwell_s": 360,
      "excluded_closed": 1,
      "id": "prescription",
      "kind": "category",
      "matched_categories": [
        "pharmacy"
      ]
    },
    {
      "candidates_costed": 6,
      "candidates_in_corridor": 20,
      "category": "petrol station",
      "dwell_s": 300,
      "excluded_closed": 0,
      "id": "petrol",
      "kind": "category",
      "matched_categories": [
        "fuel"
      ]
    }
  ],
  "eta_calibration": {
    "applied": true,
    "cells_calibrated": 179,
    "cells_not_walked": 0,
    "cells_unchanged": 17,
    "method": "gb_typical_delay_v1",
    "note": "Local roads in Great Britain carry no live traffic data, so the time on them includes the junction and congestion delay a typical car meets at this time of day, from Department for Transport statistics, on top of the routing engine's free-flowing time: the same delay POST /route adds, applied to the path each cell was costed on. It is a typical figure, not live traffic. Motorways, roads covered by National Highways traffic data and roads outside Great Britain keep the engine's own time.",
    "period": "all_day",
    "sources": [
      "Department for Transport, tables CGN0503 and CGN0504: average speed and delay on local 'A' roads in England, urban and rural, by time of day, rolling year to June 2026"
    ]
  },
  "feasible": true,
  "geometry_polyline6": "}ij}_BtxqGhBTvEh@vFx@cAfSlFzBr@ZbM~Dh@lFj@YnIxBtPjGfPvFjDlApAj@",
  "opening_hours": {
    "candidates_costed": 12,
    "candidates_with_a_tag": 4,
    "clock_change_possible_from": "2026-11-06T01:00:00Z",
    "coverage_note": "Opening hours come from OpenStreetMap's `opening_hours` tag, which most places do not carry: on a British sample of the production index taken on 21 September 2026, 41 % of places carried one at all. A stop with no usable tag is planned and flagged, never quietly treated as open.",
    "evaluated_in_offset": "+00:00",
    "policy": "A stop the map's opening-hours tag proves shut at the time you would arrive is dropped and the chain re-solved. No stop is ever reported as known-open: `open_on_the_tag` means the tag covers your arrival time, which is evidence and not a promise.",
    "stops_closed_on_the_tag": 0,
    "stops_excluded_as_closed": 1,
    "stops_open_on_the_tag": 0,
    "stops_with_no_usable_tag": 2
  },
  "plan": {
    "final_leg": {
      "distance_m": 4114,
      "drive_s": 443.6
    },
    "stops": [
      {
        "arrive": "2026-11-05T20:55:47Z",
        "category": "petrol station",
        "depart": "2026-11-05T21:00:47Z",
        "distance_m": 5927,
        "drive_s": 814.2,
        "dwell_s": 300,
        "errand": "petrol",
        "hours": {
          "message": "The map carries no usable opening hours for this place, so the plan does not know whether it is open when you would arrive.",
          "source": "OpenStreetMap opening_hours tag, as held in this deployment's place index",
          "tag": null,
          "verdict": "unknown"
        },
        "kind": "category",
        "place": {
          "id": "osm:w608512509:poi",
          "label": "Valero, Falmer Road, Brighton, Knepp Wildland, United Kingdom",
          "lat": 50.83624867030552,
          "lon": -0.07496869883366994,
          "name": "Valero",
          "off_route_m": 70,
          "source": "index",
          "type": "fuel"
        }
      },
      {
        "arrive": "2026-11-05T21:01:29Z",
        "category": "pharmacy",
        "depart": "2026-11-05T21:07:29Z",
        "distance_m": 268,
        "drive_s": 41.5,
        "dwell_s": 360,
        "errand": "prescription",
        "hours": {
          "message": "The map carries no usable opening hours for this place, so the plan does not know whether it is open when you would arrive.",
          "source": "OpenStreetMap opening_hours tag, as held in this deployment's place index",
          "tag": null,
          "verdict": "unknown"
        },
        "kind": "category",
        "place": {
          "id": "osm:w1387527988:poi",
          "label": "Well Pharmacy, Warren Way 13, BN2 6PH, Brighton, Knepp Wildland, United Kingdom",
          "lat": 50.837354278564455,
          "lon": -0.07435394078493118,
          "name": "Well Pharmacy",
          "off_route_m": 63.8,
          "source": "index",
          "type": "pharmacy"
        }
      }
    ],
    "total_distance_m": 10309,
    "total_drive_s": 1299.3,
    "total_duration_s": 1959.3,
    "total_dwell_s": 660
  },
  "search": {
    "candidates_costed": 12,
    "category_radius_m": 15000,
    "corridor_max_detour_minutes": 12,
    "note": "Candidates come from the corridor of the direct route. A place further off it than the corridor allows was never considered, so an empty or infeasible answer is a statement about this corridor and this index, never about what exists on the ground.",
    "per_errand_cap": 6,
    "sample_points": 9,
    "total_cap": 24
  },
  "slack_s": 64806.9,
  "usage": "Plan before setting off, or let a passenger do it. Stating a request like this is not something a driver can do at the wheel: handheld phone use while driving has carried six points in Great Britain since March 2022, and the offence covers operating an app.",
  "verification": "Every stop is either a place held in this deployment's map index, with its id, or a coordinate you supplied. No place in this response was generated, and every time came from the routing engine."
}
400Invalid request, including an instant more than 366 days from now and an arrive_by more than 18 hours after departure

application/problem+json · Problem

401Missing or invalid API key. Also: No key, or a missing, malformed, unknown, revoked or expired one (unauthorized); the body carries signup, docs and a one-call hint for a free key

application/problem+json · Problem

402Beyond the included volume with no way to pay this call: an x402 body naming how to pay (provisional key past its allowance, prepaid balance below the price, or a rejected X-PAYMENT). Not problem+json

application/json · X402PaymentRequired

403The key may not call this: a browser key outside its scope (scope-insufficient), a request from an origin the key does not allow (origin-not-allowed), or an account restriction (forbidden)

application/problem+json · Problem

413The request declares a body larger than 1048576 bytes (plain text, not problem+json)

text/plain

422The chain is too large or too spread out for the engine's matrix limits

application/problem+json · Problem

429Quota or rate limit exceeded. Also: Per-key rate limit (rate-limited, with Retry-After), or monthly quota exhausted with no payment path (quota-exceeded)

application/problem+json · Problem

501A category errand on a deployment with no first-party place index (SN_GEOCODE_DIR unset)

application/problem+json · Problem

502Upstream routing engine failed

application/problem+json · Problem

504The request did not finish within the gateway's request deadline (request-timeout)

application/problem+json · Problem

Error bodies follow the shared problem model documented on API conventions.

Also under Routing

Generated from https://api.mapmap.ai/openapi.json on 2026-11-06 · operationId post_errand_plan