Skip to content

Run a pickup and delivery courier

Two couriers in Montréal, starting from a hub downtown, with a morning to cover: 09:00 to 11:00. Six orders, each picked up in one neighbourhood and dropped off in another. A pickup and its delivery are one shipment, with both pickups and deliveries, so the same courier carries the order from one to the other, pickup first.

Each order is a number of boxes (loadDemands), and a courier holds at most four at once (loadLimits). That is what forces the routes to interleave: a courier picks up, drops off, and picks up again rather than collecting everything first. Three orders have a delivery time window. order-306 has a penaltyCost of 40, which makes it optional: the solver may leave it out if serving it would cost more than 40, and it would then be listed in skippedShipments.

What to read in the answer:

  • visits[].isPickup tells the pickup visit of an order from its delivery.
  • metrics.performedShipmentCount against the six orders tells you whether anything was skipped.
  • X-Unmap-Billable-Shipments counts only the orders that were delivered.

Send it

Save the body below as pickup-delivery-courier.json and post it to /optimize. Set UNMAP_API_KEY to your API key.

curl -X POST "https://api.unmap.dev/optimize" \
  -H "X-API-Key: $UNMAP_API_KEY" \
  -H "Content-Type: application/json" \
  --data @pickup-delivery-courier.json

The body uses straight-line travel at a fixed speed (useGeodesicDistances with geodesicMetersPerSecond), so it needs no road network. Delete those two fields to plan with driving times from the Canada road network instead. Times are UTC: 13:00:00Z is 09:00 in Montréal in October. "timeout": "10s" is the longest solve on the Hobby plan, and the request stays inside Hobby's limits (25 shipments, 5 vehicles). The solver may return a different visit order between runs.

This body is a file in Unmap's conformance suite, which validates it and checks the solver's answers for feasibility. After you change it, send it with "solvingMode": "VALIDATE_ONLY" to check its fields, then solve it to check that your constraints fit together.

{
  "useGeodesicDistances": true,
  "geodesicMetersPerSecond": 7,
  "timeout": "10s",
  "model": {
    "globalStartTime": "2026-10-13T13:00:00Z",
    "globalEndTime": "2026-10-13T23:00:00Z",
    "shipments": [
      {
        "label": "order-301",
        "pickups": [
          {
            "arrivalLocation": { "latitude": 45.522, "longitude": -73.58 },
            "duration": "180s"
          }
        ],
        "deliveries": [
          {
            "arrivalLocation": { "latitude": 45.507, "longitude": -73.554 },
            "duration": "240s",
            "timeWindows": [{ "endTime": "2026-10-13T14:00:00Z" }]
          }
        ],
        "loadDemands": {
          "boxes": { "amount": "2" }
        }
      },
      {
        "label": "order-302",
        "pickups": [
          {
            "arrivalLocation": { "latitude": 45.525, "longitude": -73.6 },
            "duration": "180s"
          }
        ],
        "deliveries": [
          {
            "arrivalLocation": { "latitude": 45.492, "longitude": -73.56 },
            "duration": "240s"
          }
        ],
        "loadDemands": {
          "boxes": { "amount": "1" }
        }
      },
      {
        "label": "order-303",
        "pickups": [
          {
            "arrivalLocation": { "latitude": 45.543, "longitude": -73.585 },
            "duration": "180s"
          }
        ],
        "deliveries": [
          {
            "arrivalLocation": { "latitude": 45.478, "longitude": -73.585 },
            "duration": "240s",
            "timeWindows": [{ "endTime": "2026-10-13T14:30:00Z" }]
          }
        ],
        "loadDemands": {
          "boxes": { "amount": "3" }
        }
      },
      {
        "label": "order-304",
        "pickups": [
          {
            "arrivalLocation": { "latitude": 45.545, "longitude": -73.62 },
            "duration": "180s"
          }
        ],
        "deliveries": [
          {
            "arrivalLocation": { "latitude": 45.544, "longitude": -73.54 },
            "duration": "240s"
          }
        ],
        "loadDemands": {
          "boxes": { "amount": "1" }
        }
      },
      {
        "label": "order-305",
        "pickups": [
          {
            "arrivalLocation": { "latitude": 45.52, "longitude": -73.61 },
            "duration": "180s"
          }
        ],
        "deliveries": [
          {
            "arrivalLocation": { "latitude": 45.46, "longitude": -73.57 },
            "duration": "240s",
            "timeWindows": [{ "startTime": "2026-10-13T14:00:00Z", "endTime": "2026-10-13T15:00:00Z" }]
          }
        ],
        "loadDemands": {
          "boxes": { "amount": "2" }
        }
      },
      {
        "label": "order-306",
        "pickups": [
          {
            "arrivalLocation": { "latitude": 45.473, "longitude": -73.617 },
            "duration": "180s"
          }
        ],
        "deliveries": [
          {
            "arrivalLocation": { "latitude": 45.5017, "longitude": -73.5673 },
            "duration": "240s"
          }
        ],
        "loadDemands": {
          "boxes": { "amount": "2" }
        },
        "penaltyCost": 40
      }
    ],
    "vehicles": [
      {
        "label": "courier-1",
        "startLocation": { "latitude": 45.5, "longitude": -73.57 },
        "endLocation": { "latitude": 45.5, "longitude": -73.57 },
        "startTimeWindows": [{ "startTime": "2026-10-13T13:00:00Z", "endTime": "2026-10-13T13:30:00Z" }],
        "endTimeWindows": [{ "endTime": "2026-10-13T15:00:00Z" }],
        "loadLimits": {
          "boxes": { "maxLoad": "4" }
        },
        "costPerHour": 25,
        "costPerKilometer": 0.5
      },
      {
        "label": "courier-2",
        "startLocation": { "latitude": 45.5, "longitude": -73.57 },
        "endLocation": { "latitude": 45.5, "longitude": -73.57 },
        "startTimeWindows": [{ "startTime": "2026-10-13T13:00:00Z", "endTime": "2026-10-13T13:30:00Z" }],
        "endTimeWindows": [{ "endTime": "2026-10-13T15:00:00Z" }],
        "loadLimits": {
          "boxes": { "maxLoad": "4" }
        },
        "costPerHour": 25,
        "costPerKilometer": 0.5
      }
    ]
  }
}

Next steps