API
No key, no account, no rate-limit tier to buy. This is the same endpoint the map calls — there is no private path from the app to the solver, so the API is exercised on every plan and cannot quietly rot.
Plan a route
One POST. The depot, your stops, how many vehicles. Everything else has a sensible default.
curl -X POST https://your-host/api/v1/solve \
-H 'Content-Type: application/json' \
-d '{
"depot": { "lat": 48.8443, "lng": 2.3743 },
"stops": [
{ "id": "s1", "label": "Louvre", "lat": 48.8606, "lng": 2.3376 },
{ "id": "s2", "label": "Notre-Dame", "lat": 48.8530, "lng": 2.3499 }
],
"vehicles": [{ "id": "v1" }, { "id": "v2" }],
"options": { "objective": "balanced", "roundTrip": true }
}'You get back the routes, in order, with the numbers already worked out:
{
"routes": [{
"vehicleId": "v1",
"vehicleIndex": 0,
"stops": [{
"stopId": "s1",
"stopIndex": 0,
"sequence": 0,
"arrivalOffset": 412, // seconds from departure
"distanceFromPrevious": 3820.4 // metres
}],
"totalDistance": 7640,
"totalDuration": 1224
}],
"summary": {
"totalDistance": 7640,
"longestRouteDistance": 7640,
"vehiclesUsed": 1,
"stopsServed": 2,
"baselineDistance": 8100 // your list order, for comparison
},
"meta": {
"solver": "greedy",
"solveTimeMs": 3,
"matrixSource": "osrm", // or "haversine" — see below
"warnings": []
}
}Endpoints
| Endpoint | What it does |
|---|---|
GET /api/v1/solve | Available engines and objectives. The app's own selector reads this, so it never hardcodes engine names. |
POST /api/v1/solve | Plan routes. The one that matters. |
POST /api/v1/geocode | Addresses to coordinates, up to 10 at a time. Returns a precision grade and any alternatives. |
POST /api/v1/route-geometry | Road shapes for drawing. Separate from the plan on purpose: a failure here costs you the drawing, not the routes. |
POST /api/v1/share | Encode a plan into a URL. There is no database — the link is the storage. |
GET /api/health | Liveness and which services are configured. |
Options
| Field | Default | Meaning |
|---|---|---|
objective | balanced | balanced evens the workload; distance minimises total kilometres and may leave vehicles idle. |
roundTrip | true | False means each vehicle finishes at its last stop. |
defaultServiceTime | 300 | Seconds at each stop. Overridable per stop. |
solver | greedy | greedy is always available. ortools needs a sidecar; ask GET /api/v1/solve what is on. |
Errors
Every error is a code and a sentence written for a person, never a stack trace.
{
"error": {
"code": "TOO_MANY_STOPS",
"message": "This plan has 140 stops. The limit is 99. Split it into two plans."
}
}| Code | HTTP | Cause |
|---|---|---|
INVALID_PROBLEM | 400 | Malformed body. detail names the fields. |
TOO_MANY_STOPS | 413 | Over 99 stops. |
SOLVER_UNAVAILABLE | 400 | That engine is not set up here. |
SOLVER_TIMEOUT | 504 | The engine took too long. |
INFEASIBLE | 400 | No plan satisfies the constraints. |
RATE_LIMITED | 429 | Too many requests. Retry-After says how long. |
Two things worth knowing
It degrades rather than failing. If road distances are unavailable you still get a plan, computed from straight-line estimates, with meta.matrixSource set to haversine and a warning attached. Check that field before presenting distances as fact.
Vehicles are always an array of objects, even when you only care about the count. Heterogeneous fleets — two vans and a bike — are the first thing real operators ask for, and an integer count would be a breaking change later.
Rate limits
Per IP, per minute: 20 plans, 60 geocodes. Every response carries RateLimit-Limit and RateLimit-Remaining so you can slow down before being refused rather than after.