Carpathian Maps

API reference

The directions API answers walk, bike, drive and transit routes and isochrones from your backend.

All bodies are JSON. Every routing response carries Cache-Control: no-store.

Auth

/v1/route and /v1/isochrone need the shared key from MAPS_API_KEY:

Authorization: Bearer <key>

A missing or wrong key answers 401 {"error": "Unauthorized."}. The key is meant for a backend; never ship it to a browser or app.

POST /v1/route

One route between two points.

{
  "origin": {"lat": 41.5868, "lng": -93.625},
  "destination": {"lat": 41.6005, "lng": -93.6091},
  "mode": "walk"
}

mode is walk, bike, drive (Valhalla) or transit (OpenTripPlanner).

Success, 200:

{
  "route": {
    "mode": "transit",
    "duration_s": 1500,
    "distance_m": 4551,
    "path": [[41.5868, -93.625], [41.5871, -93.6249]],
    "steps": [
      {
        "instruction": "Walk to 6th Ave & Keo Way",
        "distance_m": 350,
        "duration_s": 300,
        "travel": "walk",
        "line": null,
        "depart_at": "2026-09-21T14:13:20+00:00",
        "arrive_at": "2026-09-21T14:18:20+00:00"
      },
      {
        "instruction": "Take 6 to DART Central Station",
        "distance_m": 4201,
        "duration_s": 1100,
        "travel": "bus",
        "line": "6",
        "depart_at": "2026-09-21T14:20:00+00:00",
        "arrive_at": "2026-09-21T14:38:20+00:00"
      }
    ]
  }
}

POST /v1/isochrone

Areas reachable from a point within each travel time.

{
  "origin": {"lat": 41.5868, "lng": -93.625},
  "mode": "walk",
  "minutes": [15, 30]
}

minutes holds 1 to 4 different whole numbers from 1 to 120.

Success, 200, largest area first:

{
  "isochrones": [
    {"minutes": 30, "geometry": {"type": "Polygon", "coordinates": [[[-93.6, 41.5], ...]]}},
    {"minutes": 15, "geometry": {"type": "Polygon", "coordinates": [[[-93.6, 41.5], ...]]}}
  ]
}

geometry is a GeoJSON Polygon or MultiPolygon, as the engine returns it ([lng, lat] order). An area the engine returns without a geometry is left out, so the list can be shorter than minutes or empty.

Errors

Every error body is {"error": "<short message>"}.

StatuserrorWhen
401Unauthorized.Missing or wrong key
405Wrong method
413Request body is too large.Body over 4 KB
422(names the bad field)Body is not one JSON object, has an unknown field, a point is missing or out of range (lat -90..90, lng -180..180), unknown mode, or bad minutes
422No route was found./v1/route only: the engine answered and found no route
503Directions are unavailable right now.The engine timed out (5 s for a route, 15 s for an isochrone), is down, or answered with an error

GET /healthz

No key. Always 200, with whether each engine answers HTTP:

{"valhalla": true, "otp": false}

Map files

GET or HEAD /map/<path> serves files from $MAPS_DATA_DIR/map. No key. Range requests, ETag, CORS and Cache-Control: public, max-age=86400 are supported. Folders are never listed.

Logging

The server logs method, path, status and duration for each request except map files, viewer assets and /healthz, with the error text for any refusal. Every error text is the server's own fixed wording from the table above, never part of what the caller sent. When an engine fails it also logs the engine name with a status or failure kind. It never logs bodies, coordinates or query strings. A 401 is logged as WARN, a 5xx as ERROR; see Logs in the README for the files.