# Route Optimizer: Vehicle Routing & Delivery Route Planner (`jumblemog/route-optimizer`) Actor

Plan optimal delivery and service routes for one or more vehicles with Google OR-Tools: capacities, time windows, working hours, pickups and deliveries, skills. Stops from a list, CSV, Google Sheets or Google Maps Scraper results. Maps, times, Google Maps links. For AI agents.

- **URL**: https://apify.com/jumblemog/route-optimizer.md
- **Developed by:** [Jumble Mog](https://apify.com/jumblemog) (community)
- **Categories:** AI, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Route Optimizer: Vehicle Routing & Delivery Route Planner

**Plan the best routes for your vehicles and get every stop in order with its arrival time.** Give it a list of stops (a JSON list, CSV, a Google Sheet, a link or an Apify dataset such as Google Maps Scraper results) and how many vehicles you have, and it works out which vehicle visits which stops in which order, keeping to **capacities, time windows, service times, working hours, pickups and deliveries, skills and route limits**. Every plan comes back with **each route's stops and arrival times, loads, Google Maps navigation links, a route map, a CSV, a GeoJSON file, the distance saved and the reason for every stop that couldn't fit**. It uses Google's [OR-Tools](https://developers.google.com/optimization/routing), the open-source solver behind many commercial route planners. Made for delivery businesses, field service teams, sales reps, logistics developers, automations and AI agents that need real route optimization from one API call.

![A route plan made by this Actor: two vans leaving a depot in Chicago's West Loop, one serving eight stops on the south side and the other six on the north side, each stop numbered in visiting order, with a legend giving each route's distance and stop count and a scale bar](https://api.apify.com/v2/key-value-stores/Km5b4PG3kYHdZSb1E/records/route-optimizer-example.png?signature=77dThT9azr4JIsumBNLG)

### What it does

- **The traveling salesman problem**: the shortest round trip (or one-way path) through any number of places, up to 1,000.
- **Vehicle routing for a fleet**: splits the stops between several vehicles and orders each route, using as few vehicles as the constraints need. Vehicles can differ in capacity, start and end point, working hours, skills and cost.
- **Capacities**: each stop's demand (parcels, kg, crates, seats) against each vehicle's capacity, in one unit or several at once, such as weight and volume.
- **Time windows**: stops that must be served between set times, several windows per stop if needed, with service time at each stop. Vehicles wait when they arrive early, and the plan avoids long waits when a short detour does.
- **Working hours and route limits**: shift start and end, the longest route in hours or distance (an electric van's range) and the most stops per vehicle.
- **Pickups and deliveries**: loads carried from one place to another on the same vehicle, pickup first, as couriers and food delivery work; and returns collected on delivery routes.
- **Skills and assigned vehicles**: stops that need a refrigerated van, a lift or a certified technician only go to vehicles that have it, and a stop can be fixed to one vehicle.
- **Objectives**: the shortest total distance, the least total time, balanced routes (even workloads), the fewest vehicles, or the lowest cost from your cost per km, per hour and per vehicle.
- **Never stuck**: when not every stop fits, it plans the rest and lists the ones left out with the reason, such as "the vehicles are full: together they carry 50, and the stops need 60" or "its time window (06:00-07:00) closes before any vehicle can get there".
- **Distances your way**: estimated from coordinates with no API key, from a distance or travel-time matrix you paste (an OSRM, OpenRouteService or Google Distance Matrix answer works as it is), or from your own OSRM server for real road times.
- **Results people and programs can use**: arrival, service and departure times for every stop, loads after each stop, totals, how much shorter the plan is than the order given, Google Maps links for turn-by-turn directions, a map image (PNG, SVG, PDF or interactive HTML), a CSV of every stop, a GeoJSON file of the routes and a link that opens them on an interactive street map.
- **Many plans in one run**: one per day, region or depot, up to 100, each with its own stops and settings.

### Who it's for

- **Delivery businesses**: florists, bakeries, pharmacies, grocers, furniture and appliance stores planning tomorrow's drops for a few vans.
- **Field service teams**: plumbers, electricians, cleaners, installers and inspectors with appointment windows, job durations and skills.
- **Sales reps and merchandisers**: the best order to visit 20 accounts, ending at home instead of the office.
- **Couriers and transport**: pickups and deliveries, school and staff shuttles, waste collection and recurring rounds.
- **Developers and automations**: a route optimization API for apps, spreadsheets and no-code flows (Make, n8n, Zapier), billed per use with no monthly plan.
- **AI agents and LLM apps**: language models can't solve routing problems reliably in their heads. An agent can send the stops and get back an optimized plan it can act on and explain.
- **Apify users**: plan routes straight through Google Maps Scraper results, such as every shop in a list of leads.

### How to use it

1. Put your stops in **Stops** (or paste CSV, give a link or pick an Apify dataset). Each stop needs a name and coordinates; demand, service time and a time window are optional. Mark the depot with `"type": "depot"` or set **Depot**. The form starts with 14 deliveries around Chicago for two vans, so you can click **Start** right away.
2. Set **Number of vehicles** and their **Capacity**, or list different vehicles in **Vehicles**, and the **Working hours**.
3. Click **Start**. The **Plans** view shows the map, the summary and the totals, and the **Routes** view every route's stops in order with arrival times and loads. The CSV and GeoJSON links are in each result.

You can also run it through the [Apify API](https://docs.apify.com/api/v2), on a schedule, or from Make, n8n, Zapier and other integrations.

### Examples

**The best order to visit a list of places** (a round trip from the first one):

```json
{
    "stops": [
        {"name": "Office", "lat": 40.7484, "lng": -73.9857},
        {"name": "Client A", "lat": 40.7061, "lng": -74.0087},
        {"name": "Client B", "lat": 40.7794, "lng": -73.9632},
        {"name": "Client C", "lat": 40.7308, "lng": -73.9973},
        {"name": "Client D", "lat": 40.7580, "lng": -73.9855}
    ]
}
```

**Routes through Google Maps Scraper results**, starting from a warehouse and ending wherever the last stop is:

```json
{
    "datasetId": "YOUR_DATASET_ID",
    "depot": {"name": "Warehouse", "lat": 51.5007, "lng": -0.1246},
    "vehicleCount": 3,
    "maxStopsPerVehicle": 25,
    "returnToDepot": false
}
```

**Deliveries with vans, capacities and time windows** (CSV works the same way, with one column per field):

```json
{
    "stops": [
        {"name": "Depot", "type": "depot", "lat": 41.8826, "lng": -87.6517},
        {"name": "Bakery", "lat": 41.9234, "lng": -87.7083, "demand": 5, "serviceMinutes": 6},
        {"name": "Clinic", "lat": 41.8927, "lng": -87.6200, "demand": 6, "timeWindow": "08:30-10:00"},
        {"name": "Wine bar", "lat": 41.9105, "lng": -87.6353, "demand": 4, "timeWindow": "14:00-17:00"}
    ],
    "vehicleCount": 2,
    "capacity": 40,
    "shiftStart": "08:00",
    "shiftEnd": "17:00"
}
```

**Technicians with skills, their own start points and shifts**, for the least total time:

```json
{
    "stops": [
        {"name": "Boiler repair", "lat": 52.52, "lng": 13.405, "serviceMinutes": 90, "skills": ["gas"], "timeWindow": "09:00-12:00"},
        {"name": "Socket install", "lat": 52.50, "lng": 13.42, "serviceMinutes": 30},
        {"name": "Heat pump check", "lat": 52.54, "lng": 13.38, "serviceMinutes": 60, "skills": ["heatpump"], "priority": 3}
    ],
    "vehicles": [
        {"name": "Anna", "start": {"lat": 52.49, "lng": 13.39}, "skills": ["gas", "heatpump"], "shiftStart": "08:00", "shiftEnd": "16:00"},
        {"name": "Ben", "start": {"lat": 52.55, "lng": 13.43}, "shiftStart": "10:00", "shiftEnd": "18:00"}
    ],
    "objective": "time"
}
```

**Pickups and deliveries** (each order picked up before it's delivered, on the same vehicle):

```json
{
    "depot": {"name": "Hub", "lat": 41.88, "lng": -87.65},
    "shipments": [
        {"name": "Order 17", "pickup": {"name": "Bakery", "lat": 41.92, "lng": -87.70}, "delivery": {"name": "Office", "lat": 41.88, "lng": -87.63, "timeWindow": "11:00-12:00"}, "load": 2},
        {"name": "Order 18", "pickup": {"name": "Deli", "lat": 41.90, "lng": -87.68}, "delivery": {"name": "Home", "lat": 41.94, "lng": -87.66}, "load": 1}
    ],
    "vehicleCount": 2,
    "capacity": 4
}
```

**Real road distances**: paste the answer of a matrix service (OSRM table, OpenRouteService matrix or Google Distance Matrix) with the places in the same order as the stops, in **Routing service response**, or set **OSRM server** to your own OSRM server.

**A week of plans in one run**, sharing the vehicles and settings:

```json
{
    "vehicleCount": 3,
    "capacity": 30,
    "plans": [
        {"title": "Monday", "dataUrl": "https://docs.google.com/spreadsheets/d/YOUR_SHEET/edit#gid=0"},
        {"title": "Tuesday", "dataUrl": "https://docs.google.com/spreadsheets/d/YOUR_SHEET/edit#gid=1"}
    ]
}
```

### How-to examples

Each example below is a complete input. The result line is what the run says in `summary`.

#### How to find the shortest route through a list of addresses or stops

Give coordinates; the first stop is where the round trip starts and ends.

```json
{"stops": [{"name": "Office", "lat": 40.7484, "lng": -73.9857}, {"name": "Client A", "lat": 40.7061, "lng": -74.0087},
           {"name": "Client B", "lat": 40.7794, "lng": -73.9632}, {"name": "Client C", "lat": 40.7308, "lng": -73.9973},
           {"name": "Client D", "lat": 40.7580, "lng": -73.9855}]}
```

Result: "1 vehicle serves all 4 stops: 23.6 km and 40 min of driving, estimated from straight-line distances. The route runs from 08:00 to 08:40. Visiting the stops in the order given would take 31.8 km, so this route is 26% shorter." Each route has a Google Maps link with the stops in order.

#### How to plan delivery routes for several vans with capacities and time windows

```json
{"stops": [{"name": "Depot", "type": "depot", "lat": 41.8826, "lng": -87.6517},
           {"name": "Bakery", "lat": 41.9234, "lng": -87.7083, "demand": 5, "serviceMinutes": 6},
           {"name": "Clinic", "lat": 41.8927, "lng": -87.6200, "demand": 6, "timeWindow": "08:30-10:00"},
           {"name": "Wine bar", "lat": 41.9105, "lng": -87.6353, "demand": 4, "timeWindow": "14:00-17:00"}],
 "vehicleCount": 2, "capacity": 40, "shiftStart": "08:00", "shiftEnd": "17:00"}
```

Result: "2 vehicles serve all 3 stops: 28.4 km and 49 min of driving in total, estimated from straight-line distances. Routes run from 08:24 to 14:34. Vehicle 1: 2 stops, 20.9 km, 42 min; Vehicle 2: 1 stop, 7.4 km, 13 min." Every stop gets its arrival time and the van's load.

#### How to route the places from a Google Maps scrape

```json
{"datasetId": "YOUR_DATASET_ID", "depot": {"name": "Warehouse", "lat": 51.5007, "lng": -0.1246},
 "vehicleCount": 3, "maxStopsPerVehicle": 25, "returnToDepot": false}
```

Result: the scraped places split between 3 vehicles of up to 25 stops each, with a map picture, a CSV of the stops and a link that opens the routes on an interactive map. Stop names and coordinates are read from the dataset's own fields.

#### How to let an AI agent plan routes

Add this Actor to the [Apify MCP server](https://mcp.apify.com) with `https://mcp.apify.com/?tools=jumblemog/route-optimizer`. The agent sends the stops as JSON and reads `summary`, `routes` (each with its stops in order, times and a Google Maps link) and `leftOut` for any stop that didn't fit.

### FAQ

**Does it need a Google Maps API key?** No. Distances are estimated from the coordinates. For exact road times, paste a distance matrix from OSRM, OpenRouteService or Google, or give your own OSRM server.

**Does it find addresses?** No. Stops need latitude and longitude. Google Maps scraper results and most store lists already have them.

**How many stops can it handle?** Up to 1,000 stops and 100 vehicles per plan.

**Can it skip stops that don't fit?** Yes. Stops that can't be served in the vehicles' shifts or capacity are listed in `leftOut` with the reason, and you aren't charged for them.

**Does it work with Zapier, Make or n8n?** Yes. Call the Actor with its Apify app or an HTTP request, and send the Google Maps links to the drivers.

### Input

| Field | What it's for |
|---|---|
| `stops` | The stops: `name`, `lat`, `lng` and optional `demand`, `pickup`, `serviceMinutes`, `timeWindow`, `priority`, `optional`, `skills`, `vehicle`, `address`, `id`. Field names are matched loosely (latitude, quantity, service_time, time_window, Google Maps Scraper's `location`). |
| `stopsCsv`, `dataUrl`, `datasetId` | The same stops as CSV text, a link (CSV, JSON, Excel, Google Sheets, GitHub) or an Apify dataset. |
| `depot` | Where vehicles start and finish. Or mark a stop `"type": "depot"`; otherwise the first stop is the depot. |
| `vehicleCount`, `capacity` | Identical vehicles and what each carries. |
| `vehicles` | Different vehicles: `name`, `count`, `capacity` (a number or `{"weight": 1000, "volume": 12}`), `start`, `end`, `shiftStart`, `shiftEnd`, `skills`, `maxStops`, `maxDistance`, `maxRouteHours`, `costPerKm`, `costPerHour`, `fixedCost`. |
| `shiftStart`, `shiftEnd`, `maxRouteHours`, `maxRouteDistance`, `maxStopsPerVehicle` | Working hours and route limits for all vehicles. |
| `returnToDepot`, `startAnywhere` | Round trips, or routes that end at the last stop; and routes with no fixed start. |
| `objective` | `distance` (default), `time`, `balanced`, `vehicles` or `cost`. |
| `allowSkipping` | On (default): plan what fits and list the rest with reasons. Off: fail with the reasons. |
| `shipments` | Pickup-and-delivery pairs with their load. |
| `units`, `distanceSource`, `averageSpeed`, `roadFactor` | Kilometers or miles, and how distances are worked out. |
| `distanceMatrix`, `durationMatrix`, `routingResponse`, `osrmUrl` | Your own road distances and times. |
| `costPerDistance`, `costPerHour`, `costPerVehicle` | Costs for the cheapest plan and the cost estimate. |
| `chart`, `chartFormats`, `chartTheme`, `title` | The map: PNG, SVG, PDF or interactive HTML, light or dark. |
| `timeLimitSeconds` | Solving time; 0 picks it from the size. |
| `plans` | Several plans in one run. |

### Output

One dataset item per plan (shortened here):

```json
{
    "plan": 1,
    "title": "Route plan: 2 vehicles, 14 stops",
    "status": "ok",
    "summary": "2 vehicles serve all 14 stops: 76.8 km and 2 h 12 min of driving in total, estimated from straight-line distances. Routes run from 09:12 to 14:13. Van 1: 8 stops, 43.3 km, 2 h 05 min; Van 2: 6 stops, 33.5 km, 1 h 36 min.",
    "totalDistance": 76.809,
    "totalDrivingMinutes": 131.7,
    "totalWaitingMinutes": 0,
    "totalDurationMinutes": 221.7,
    "vehiclesUsed": 2,
    "stopsServed": 14,
    "stopsLeftOut": 0,
    "routes": [
        {
            "vehicle": "Van 1",
            "stopsCount": 8,
            "distance": 43.266,
            "departure": "09:12",
            "finish": "11:17",
            "loadAtStart": 38,
            "capacity": 40,
            "utilizationPct": 95,
            "googleMapsUrls": ["https://www.google.com/maps/dir/?api=1&origin=41.882600,-87.651700&destination=41.882600,-87.651700&travelmode=driving&waypoints=41.908800,-87.679600|41.892700,-87.620000|..."],
            "stops": [
                {"sequence": 1, "name": "Wicker Park Cafe", "arrival": "09:20", "serviceStart": "09:20", "departure": "09:25", "waitMinutes": 0, "distanceFromPrevious": 4.833, "delivered": 4, "loadAfter": 34, "timeWindow": "09:00-11:00", "row": 3},
                {"sequence": 2, "name": "Streeterville Clinic", "arrival": "09:37", "serviceStart": "09:37", "departure": "09:45", "loadAfter": 28, "timeWindow": "08:30-10:00", "row": 14}
            ]
        }
    ],
    "leftOut": [],
    "comparison": null,
    "distanceSource": "straight-line distances x 1.3 for roads, at 35 km/h",
    "solver": {"status": "solved", "seconds": 3.01, "objective": "the shortest total distance, without long waits for time windows"},
    "mapUrl": "https://api.apify.com/v2/key-value-stores/.../records/plan-001-Route-plan-2-vehicles-14-stops-map.png",
    "csvUrl": "https://api.apify.com/v2/key-value-stores/.../records/plan-001-Route-plan-2-vehicles-14-stops-routes.csv",
    "geojsonUrl": "https://api.apify.com/v2/key-value-stores/.../records/plan-001-Route-plan-2-vehicles-14-stops-routes.geojson",
    "interactiveMapUrl": "https://geojson.io/#data=data:text/x-url,https%3A%2F%2Fapi.apify.com%2F...",
    "warnings": [],
    "notes": []
}
```

For a single route that serves every stop, `comparison` gives the distance of the order you gave and how much shorter the plan is. Stops left out are listed in `leftOut` with the reason. The run's `OUTPUT` record holds all plans as one JSON object.

### How it works

The plan is a vehicle routing problem solved by OR-Tools' routing library: a first solution by cheapest insertion, then guided local search, which keeps improving the routes until the solving time runs out (about 1 second for 10 stops, 8 for 100 and a minute for 1,000, a little more with time windows or pickups). Capacities, time windows, working hours and route limits are hard rules; a stop that can't be served is left out with a penalty, so the solver always returns the best plan that fits, and the reason is worked out afterwards. Vehicles leave as late as they can without arriving after a window closes, so nobody waits longer than needed.

By default, distances are estimated: the straight-line distance between coordinates times a road factor of 1.3, driven at 35 km/h (22 mph) on average. That's good for comparing orders and splitting stops between vehicles, and needs no map service; for exact road times, give a matrix from your routing service or an OSRM server.

### Accuracy

On OR-Tools' own documented examples:

| Problem | Best known | This Actor |
|---|---|---|
| Traveling salesman, 13 US cities | 7,293 miles (checked by exact search) | 7,293 miles |
| Capacitated routing, 16 stops and 4 vehicles of capacity 15 | 6,208 | 6,208 |

Every finished plan in the tests is checked against its rules: service inside each time window, loads within capacity, pickups before their deliveries on the same vehicle, and skills.

### Pricing

You pay per **route plan** and per **routed stop** (each stop on the finished routes), at the prices shown on this page, with the map, CSV, GeoJSON and Google Maps links included. Stops left out aren't charged, and plans that can't be made are never charged. If you set a maximum cost per run, the Actor checks before each plan that it fits.

### Speed

A plan through the API answers in about 10 seconds for a few dozen stops, most of it the cloud container starting and the solver searching. Larger plans take as long as the solving time: about 12 seconds for 100 stops with time windows, 45 seconds for 300 and a minute for 1,000.

### Tips and limits

- **Coordinates**: every stop needs latitude and longitude unless you give a distance matrix. To get coordinates from addresses, run a geocoder first, or use Google Maps Scraper, whose results this Actor reads as they are.
- **Times**: 09:00, 9:30 am, 14h and 2026-10-24T09:00 all work; windows can span midnight (22:00-02:00). Service times take 10, "10 min", "1h30" or "90s".
- **Demand without capacity** is reported as loads but doesn't limit anything; set **Capacity** to make it count.
- **Priorities**: when not everything fits, stops with a higher `priority` are kept first, and `optional` stops go first.
- **Limits**: 1,000 stops per plan (a shipment counts as two), 100 vehicles, 100 plans per run, files up to 30 MB, and OSRM tables up to 400 places. Solving time goes up to 30 seconds for plans of up to 50 stops, then 0.6 seconds per stop (600 for 1,000), and a run stops once plans that can't be made have taken 3 minutes.
- **Datasets**: Apify lets this Actor read only the dataset picked in its **Apify dataset** field.
- **Links**: CSV, TSV, JSON and Excel files, GitHub file links, Gist links and Google Sheets links (shared as "Anyone with the link" or published to the web). Every site's robots.txt is checked first.
- **Directions**: each route's Google Maps links hold up to 9 stops between start and end (Google's limit), so longer routes get several links in order.

### For AI agents and developers

This Actor works with the [Apify MCP server](https://mcp.apify.com), so AI agents can call it as a tool: send the stops and read `summary`, `routes` and `leftOut` from the result. A direct API call that waits for the answer:

```bash
curl -X POST "https://api.apify.com/v2/acts/jumblemog~route-optimizer/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"stops": [{"name": "A", "lat": 48.8584, "lng": 2.2945}, {"name": "B", "lat": 48.8606, "lng": 2.3376}, {"name": "C", "lat": 48.8530, "lng": 2.3499}], "chart": false}'
```

`run-sync` instead returns the run's `OUTPUT`: the status, how many plans were made and every plan as JSON.

In Python, with the `apify-client` package:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")
run = client.actor("jumblemog/route-optimizer").call(run_input={
    "stops": [
        {"name": "Depot", "type": "depot", "lat": 41.8826, "lng": -87.6517},
        {"name": "Bakery", "lat": 41.9234, "lng": -87.7083, "demand": 5},
        {"name": "Clinic", "lat": 41.8927, "lng": -87.6200, "demand": 6, "timeWindow": "08:30-10:00"},
    ],
    "vehicleCount": 2,
    "capacity": 10,
})
for plan in client.dataset(run["defaultDatasetId"]).iterate_items():
    for route in plan["routes"]:
        print(route["vehicle"], [(stop["name"], stop["arrival"]) for stop in route["stops"]])
```

### Credits

- Routing: [Google OR-Tools](https://developers.google.com/optimization) (Apache License 2.0) and [NumPy](https://numpy.org) (BSD-3-Clause).
- Data files: [openpyxl](https://openpyxl.readthedocs.io) (MIT).
- Maps: [Vega-Lite](https://vega.github.io/vega-lite/) (BSD-3-Clause) through [vl-convert](https://github.com/vega/vl-convert) (BSD-3-Clause), with the Inter font (SIL Open Font License). Directions open in Google Maps and street maps at [geojson.io](https://geojson.io).
- Test problems: the traveling salesman and capacitated routing examples from the OR-Tools documentation.

# Actor input Schema

## `stops` (type: `array`):

The places to visit, as objects: {"name": "Lincoln Park Market", "lat": 41.9214, "lng": -87.6513}. Optional per stop: demand (a number, or {"weight": 120, "volume": 2} for several capacity kinds), pickup (amount collected and brought back to the depot), serviceMinutes, timeWindow ("09:00-11:00", several as "09:00-11:00, 14:00-16:00", or windowStart and windowEnd), priority (higher is kept first when not everything fits), optional (true: may be skipped), skills (such as \["fridge"]: only vehicles with them), vehicle (must go on this vehicle), address and id. Mark the depot with "type": "depot", or leave it out and the first stop is the depot. Field names are matched loosely, so latitude/longitude, quantity, service_time or time_window also work.

## `stopsCsv` (type: `string`):

CSV or TSV text with a header row, such as name,lat,lng,demand,timeWindow on the first line and Lincoln Park Market,41.9214,-87.6513,6,09:00-11:00 on the next. The same columns as Stops work.

## `dataUrl` (type: `string`):

A link to a CSV, TSV, JSON or Excel (.xlsx) file of stops. Google Sheets share links (shared as "Anyone with the link"), GitHub file links and JSON API answers work. The site's robots.txt is respected.

## `datasetId` (type: `string`):

Plan routes through the items of a dataset in your account, such as Google Maps Scraper results (title, address and location are read as they are). Up to 1,000 stops; the tool gets read access to this dataset only.

## `depot` (type: `object`):

Where vehicles start and finish: {"name": "Warehouse", "lat": 41.88, "lng": -87.65}, or {"name": "Warehouse"} to use the stop of that name. Leave empty to use the stop marked "type": "depot", or else the first stop.

## `startAnywhere` (type: `boolean`):

For routes that end at their last stop (Return to depot off): each route starts at whichever stop makes it shortest. Use it to find the best order to visit places with no fixed starting point.

## `vehicleCount` (type: `integer`):

How many identical vehicles (or drivers) are available. The plan uses as many as it needs, up to this number.

## `capacity` (type: `number`):

How much each vehicle carries, in the same unit as the stops' demand (parcels, kg, crates, seats). Empty: no limit. For several kinds (weight and volume) set capacity objects in Vehicles.

## `vehicles` (type: `array`):

Different vehicles, each an object: {"name": "Truck", "count": 2, "capacity": 1200, "start": {"lat": 41.9, "lng": -87.7}, "end": "depot", "shiftStart": "07:00", "shiftEnd": "15:00", "skills": \["fridge"], "maxStops": 20, "maxDistance": 150, "costPerKm": 0.6, "costPerHour": 25, "fixedCost": 40}. capacity can be an object for several kinds: {"weight": 1000, "volume": 12}. start and end default to the depot; end can also be "anywhere" (finish at the last stop). Replaces Number of vehicles.

## `shiftStart` (type: `string`):

When vehicles can leave, such as 08:00 or 8:30 am. Vehicles leave as late as they can without arriving after a time window or finishing later.

## `shiftEnd` (type: `string`):

When vehicles must be back (or, for routes that don't return, finished), such as 17:00. Empty: no end.

## `maxRouteHours` (type: `number`):

The most time a route may take from departure to return, waiting and service included.

## `maxRouteDistance` (type: `number`):

The most distance one route may cover, in the plan's units (km or miles), such as an electric van's range.

## `maxStopsPerVehicle` (type: `integer`):

The most stops one vehicle may visit.

## `returnToDepot` (type: `boolean`):

On: routes end back at the depot. Off: each route ends at its last stop (drivers going home, one-way trips).

## `objective` (type: `string`):

What the plan minimizes. With time windows, the distance, balanced and fewest-vehicles plans also avoid long waits when a short detour does.

## `allowSkipping` (type: `boolean`):

On: when not every stop fits (capacity, time windows, working hours), plan the rest and list the stops left out with the reason. Off: fail with the reasons instead. Stops marked optional can always be left out.

## `shipments` (type: `array`):

Loads to carry from one place to another on the same vehicle, pickup first (couriers, food delivery, moving): \[{"name": "Order 17", "pickup": {"name": "Bakery", "lat": 41.92, "lng": -87.70}, "delivery": {"name": "Office", "lat": 41.88, "lng": -87.63, "timeWindow": "11:00-12:00"}, "load": 2}]. Pickup and delivery take the same optional fields as stops.

## `timeLimitSeconds` (type: `integer`):

How long the solver searches for a better plan. 0: chosen from the size, about 1 s for 10 stops, 8 s for 100 and 60 s for 1,000. At most 30 s up to 50 stops, then 0.6 s per stop, since longer searches don't pay off.

## `units` (type: `string`):

Units for distances in the results and for distance settings.

## `distanceSource` (type: `string`):

Estimated: the straight-line distance times Road factor, driven at Average speed. Good for comparing orders; a road network gives exact times.

## `averageSpeed` (type: `number`):

For estimated travel times: average speed including stops at lights, in km/h (mph with miles). Default 35 km/h (22 mph), typical for city driving; try 60 to 80 for rural or highway routes.

## `roadFactor` (type: `number`):

For estimated distances: how much longer road trips are than straight lines. 1.3 suits most cities; 1.2 for grid streets, 1.5 for rivers, hills or coastlines.

## `distanceMatrix` (type: `array`):

Road distances between every pair of places, one row and one column per place, in km (miles with Distance units miles): the depot first unless it's one of the stops, then the stops in the order given, then each shipment's pickup and delivery. null marks pairs that can't be reached.

## `durationMatrix` (type: `array`):

Driving minutes between every pair of places, in the same order as the distance matrix. Without it, times come from the distances at Average speed.

## `routingResponse` (type: `object`):

A matrix response pasted as it is: an OSRM table, OpenRouteService matrix or Google Distance Matrix API answer, with places in the same order as for the distance matrix. Meters and seconds are converted.

## `osrmUrl` (type: `string`):

The address of an OSRM routing server you're allowed to use, such as https://osrm.example.com, for real road distances and times (one table request per plan, up to 400 places).

## `osrmProfile` (type: `string`):

The OSRM profile: driving, car, bike or foot, as your server names them.

## `costPerDistance` (type: `number`):

Fuel, wear and tolls per km, or per mile with Distance units miles.

## `costPerHour` (type: `number`):

Driver and vehicle cost per hour of route time, waiting and service included.

## `costPerVehicle` (type: `number`):

A fixed cost for each vehicle that goes out, such as a day's rental or a driver's minimum pay.

## `chart` (type: `boolean`):

Draw a map of the routes: each vehicle's route in its own color with numbered stops, the depot, the stops left out and a scale bar.

## `chartFormats` (type: `array`):

The map's file formats. Each plan also gets a CSV of every stop and a GeoJSON file of the routes.

## `chartTheme` (type: `string`):

The map's colors.

## `title` (type: `string`):

The plan's title on the map and in the results, such as Monday deliveries.

## `plans` (type: `array`):

Many plans in one run, each an object with its own stops and settings: \[{"title": "Monday", "stops": \[...]}, {"title": "Tuesday", "datasetId": "...", "vehicleCount": 3}]. Settings outside plans apply to every plan, and a plan with its own stops doesn't use the shared stops or depot.

## Actor input object example

```json
{
  "stops": [
    {
      "name": "West Loop Depot",
      "type": "depot",
      "lat": 41.8826,
      "lng": -87.6517
    },
    {
      "name": "Lincoln Park Market",
      "lat": 41.9214,
      "lng": -87.6513,
      "demand": 6,
      "serviceMinutes": 8
    },
    {
      "name": "Wicker Park Cafe",
      "lat": 41.9088,
      "lng": -87.6796,
      "demand": 4,
      "serviceMinutes": 5,
      "timeWindow": "09:00-11:00"
    },
    {
      "name": "Logan Square Bakery",
      "lat": 41.9234,
      "lng": -87.7083,
      "demand": 5,
      "serviceMinutes": 6
    },
    {
      "name": "Lakeview Florist",
      "lat": 41.9434,
      "lng": -87.6553,
      "demand": 3,
      "serviceMinutes": 5
    },
    {
      "name": "Uptown Pharmacy",
      "lat": 41.9665,
      "lng": -87.6533,
      "demand": 7,
      "serviceMinutes": 10,
      "timeWindow": "13:00-16:00"
    },
    {
      "name": "Andersonville Books",
      "lat": 41.98,
      "lng": -87.6683,
      "demand": 2,
      "serviceMinutes": 5
    },
    {
      "name": "Hyde Park Deli",
      "lat": 41.7943,
      "lng": -87.5907,
      "demand": 8,
      "serviceMinutes": 10
    },
    {
      "name": "Bronzeville Grocery",
      "lat": 41.8169,
      "lng": -87.6176,
      "demand": 6,
      "serviceMinutes": 8
    },
    {
      "name": "Chinatown Tea House",
      "lat": 41.8521,
      "lng": -87.6325,
      "demand": 3,
      "serviceMinutes": 5,
      "timeWindow": "10:00-12:00"
    },
    {
      "name": "Pilsen Taqueria",
      "lat": 41.8564,
      "lng": -87.66,
      "demand": 5,
      "serviceMinutes": 6
    },
    {
      "name": "Bridgeport Hardware",
      "lat": 41.8381,
      "lng": -87.651,
      "demand": 4,
      "serviceMinutes": 5
    },
    {
      "name": "South Loop Gym",
      "lat": 41.8569,
      "lng": -87.6243,
      "demand": 2,
      "serviceMinutes": 4
    },
    {
      "name": "Streeterville Clinic",
      "lat": 41.8927,
      "lng": -87.62,
      "demand": 6,
      "serviceMinutes": 8,
      "timeWindow": "08:30-10:00"
    },
    {
      "name": "Old Town Wine Bar",
      "lat": 41.9105,
      "lng": -87.6353,
      "demand": 4,
      "serviceMinutes": 5,
      "timeWindow": "14:00-17:00"
    }
  ],
  "startAnywhere": false,
  "vehicleCount": 2,
  "capacity": 40,
  "shiftStart": "08:00",
  "shiftEnd": "17:00",
  "returnToDepot": true,
  "objective": "distance",
  "allowSkipping": true,
  "timeLimitSeconds": 0,
  "units": "km",
  "distanceSource": "auto",
  "roadFactor": 1.3,
  "osrmProfile": "driving",
  "chart": true,
  "chartFormats": [
    "png"
  ],
  "chartTheme": "light"
}
```

# Actor output Schema

## `results` (type: `string`):

One item per plan: the map, the summary and the totals, or the error.

## `details` (type: `string`):

Each plan's routes with their stops in order, arrival times and loads, and the stops left out.

## `output` (type: `string`):

The OUTPUT record: every plan with its routes in one JSON object.

## `files` (type: `string`):

The maps (PNG, SVG, PDF, HTML), CSV and GeoJSON files and OUTPUT in the run's key-value store.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "stops": [
        {
            "name": "West Loop Depot",
            "type": "depot",
            "lat": 41.8826,
            "lng": -87.6517
        },
        {
            "name": "Lincoln Park Market",
            "lat": 41.9214,
            "lng": -87.6513,
            "demand": 6,
            "serviceMinutes": 8
        },
        {
            "name": "Wicker Park Cafe",
            "lat": 41.9088,
            "lng": -87.6796,
            "demand": 4,
            "serviceMinutes": 5,
            "timeWindow": "09:00-11:00"
        },
        {
            "name": "Logan Square Bakery",
            "lat": 41.9234,
            "lng": -87.7083,
            "demand": 5,
            "serviceMinutes": 6
        },
        {
            "name": "Lakeview Florist",
            "lat": 41.9434,
            "lng": -87.6553,
            "demand": 3,
            "serviceMinutes": 5
        },
        {
            "name": "Uptown Pharmacy",
            "lat": 41.9665,
            "lng": -87.6533,
            "demand": 7,
            "serviceMinutes": 10,
            "timeWindow": "13:00-16:00"
        },
        {
            "name": "Andersonville Books",
            "lat": 41.98,
            "lng": -87.6683,
            "demand": 2,
            "serviceMinutes": 5
        },
        {
            "name": "Hyde Park Deli",
            "lat": 41.7943,
            "lng": -87.5907,
            "demand": 8,
            "serviceMinutes": 10
        },
        {
            "name": "Bronzeville Grocery",
            "lat": 41.8169,
            "lng": -87.6176,
            "demand": 6,
            "serviceMinutes": 8
        },
        {
            "name": "Chinatown Tea House",
            "lat": 41.8521,
            "lng": -87.6325,
            "demand": 3,
            "serviceMinutes": 5,
            "timeWindow": "10:00-12:00"
        },
        {
            "name": "Pilsen Taqueria",
            "lat": 41.8564,
            "lng": -87.66,
            "demand": 5,
            "serviceMinutes": 6
        },
        {
            "name": "Bridgeport Hardware",
            "lat": 41.8381,
            "lng": -87.651,
            "demand": 4,
            "serviceMinutes": 5
        },
        {
            "name": "South Loop Gym",
            "lat": 41.8569,
            "lng": -87.6243,
            "demand": 2,
            "serviceMinutes": 4
        },
        {
            "name": "Streeterville Clinic",
            "lat": 41.8927,
            "lng": -87.62,
            "demand": 6,
            "serviceMinutes": 8,
            "timeWindow": "08:30-10:00"
        },
        {
            "name": "Old Town Wine Bar",
            "lat": 41.9105,
            "lng": -87.6353,
            "demand": 4,
            "serviceMinutes": 5,
            "timeWindow": "14:00-17:00"
        }
    ],
    "vehicleCount": 2,
    "capacity": 40,
    "shiftEnd": "17:00"
};

// Run the Actor and wait for it to finish
const run = await client.actor("jumblemog/route-optimizer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "stops": [
        {
            "name": "West Loop Depot",
            "type": "depot",
            "lat": 41.8826,
            "lng": -87.6517,
        },
        {
            "name": "Lincoln Park Market",
            "lat": 41.9214,
            "lng": -87.6513,
            "demand": 6,
            "serviceMinutes": 8,
        },
        {
            "name": "Wicker Park Cafe",
            "lat": 41.9088,
            "lng": -87.6796,
            "demand": 4,
            "serviceMinutes": 5,
            "timeWindow": "09:00-11:00",
        },
        {
            "name": "Logan Square Bakery",
            "lat": 41.9234,
            "lng": -87.7083,
            "demand": 5,
            "serviceMinutes": 6,
        },
        {
            "name": "Lakeview Florist",
            "lat": 41.9434,
            "lng": -87.6553,
            "demand": 3,
            "serviceMinutes": 5,
        },
        {
            "name": "Uptown Pharmacy",
            "lat": 41.9665,
            "lng": -87.6533,
            "demand": 7,
            "serviceMinutes": 10,
            "timeWindow": "13:00-16:00",
        },
        {
            "name": "Andersonville Books",
            "lat": 41.98,
            "lng": -87.6683,
            "demand": 2,
            "serviceMinutes": 5,
        },
        {
            "name": "Hyde Park Deli",
            "lat": 41.7943,
            "lng": -87.5907,
            "demand": 8,
            "serviceMinutes": 10,
        },
        {
            "name": "Bronzeville Grocery",
            "lat": 41.8169,
            "lng": -87.6176,
            "demand": 6,
            "serviceMinutes": 8,
        },
        {
            "name": "Chinatown Tea House",
            "lat": 41.8521,
            "lng": -87.6325,
            "demand": 3,
            "serviceMinutes": 5,
            "timeWindow": "10:00-12:00",
        },
        {
            "name": "Pilsen Taqueria",
            "lat": 41.8564,
            "lng": -87.66,
            "demand": 5,
            "serviceMinutes": 6,
        },
        {
            "name": "Bridgeport Hardware",
            "lat": 41.8381,
            "lng": -87.651,
            "demand": 4,
            "serviceMinutes": 5,
        },
        {
            "name": "South Loop Gym",
            "lat": 41.8569,
            "lng": -87.6243,
            "demand": 2,
            "serviceMinutes": 4,
        },
        {
            "name": "Streeterville Clinic",
            "lat": 41.8927,
            "lng": -87.62,
            "demand": 6,
            "serviceMinutes": 8,
            "timeWindow": "08:30-10:00",
        },
        {
            "name": "Old Town Wine Bar",
            "lat": 41.9105,
            "lng": -87.6353,
            "demand": 4,
            "serviceMinutes": 5,
            "timeWindow": "14:00-17:00",
        },
    ],
    "vehicleCount": 2,
    "capacity": 40,
    "shiftEnd": "17:00",
}

# Run the Actor and wait for it to finish
run = client.actor("jumblemog/route-optimizer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "stops": [
    {
      "name": "West Loop Depot",
      "type": "depot",
      "lat": 41.8826,
      "lng": -87.6517
    },
    {
      "name": "Lincoln Park Market",
      "lat": 41.9214,
      "lng": -87.6513,
      "demand": 6,
      "serviceMinutes": 8
    },
    {
      "name": "Wicker Park Cafe",
      "lat": 41.9088,
      "lng": -87.6796,
      "demand": 4,
      "serviceMinutes": 5,
      "timeWindow": "09:00-11:00"
    },
    {
      "name": "Logan Square Bakery",
      "lat": 41.9234,
      "lng": -87.7083,
      "demand": 5,
      "serviceMinutes": 6
    },
    {
      "name": "Lakeview Florist",
      "lat": 41.9434,
      "lng": -87.6553,
      "demand": 3,
      "serviceMinutes": 5
    },
    {
      "name": "Uptown Pharmacy",
      "lat": 41.9665,
      "lng": -87.6533,
      "demand": 7,
      "serviceMinutes": 10,
      "timeWindow": "13:00-16:00"
    },
    {
      "name": "Andersonville Books",
      "lat": 41.98,
      "lng": -87.6683,
      "demand": 2,
      "serviceMinutes": 5
    },
    {
      "name": "Hyde Park Deli",
      "lat": 41.7943,
      "lng": -87.5907,
      "demand": 8,
      "serviceMinutes": 10
    },
    {
      "name": "Bronzeville Grocery",
      "lat": 41.8169,
      "lng": -87.6176,
      "demand": 6,
      "serviceMinutes": 8
    },
    {
      "name": "Chinatown Tea House",
      "lat": 41.8521,
      "lng": -87.6325,
      "demand": 3,
      "serviceMinutes": 5,
      "timeWindow": "10:00-12:00"
    },
    {
      "name": "Pilsen Taqueria",
      "lat": 41.8564,
      "lng": -87.66,
      "demand": 5,
      "serviceMinutes": 6
    },
    {
      "name": "Bridgeport Hardware",
      "lat": 41.8381,
      "lng": -87.651,
      "demand": 4,
      "serviceMinutes": 5
    },
    {
      "name": "South Loop Gym",
      "lat": 41.8569,
      "lng": -87.6243,
      "demand": 2,
      "serviceMinutes": 4
    },
    {
      "name": "Streeterville Clinic",
      "lat": 41.8927,
      "lng": -87.62,
      "demand": 6,
      "serviceMinutes": 8,
      "timeWindow": "08:30-10:00"
    },
    {
      "name": "Old Town Wine Bar",
      "lat": 41.9105,
      "lng": -87.6353,
      "demand": 4,
      "serviceMinutes": 5,
      "timeWindow": "14:00-17:00"
    }
  ],
  "vehicleCount": 2,
  "capacity": 40,
  "shiftEnd": "17:00"
}' |
apify call jumblemog/route-optimizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jumblemog/route-optimizer"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/t243f5kcruY9v2Tn1/builds/Ete7bms6N8xtwdRBP/openapi.json
