TL;DR

Three shipments, each a pickup/delivery pair that must be served by the same vehicle, pickup before delivery. One shipment requires a skill (refrigerated) that only one of the two vans has.

Pickup & Delivery

A Shipment bundles two ShipmentSteps — pickup and delivery — each with their own location, service time, and demands. Unlike independent jobs, the solver guarantees the same vehicle performs both steps, and the pickup is always sequenced before its delivery.

Setup

Two vans (van-7, van-9) at a shared Rotterdam depot. Three shipments:

  • shipment-pkg-301: pickup at the warehouse (index 1), delivery to a customer in Rotterdam-Zuid (index 2). Demand {"weight": 40}.
  • shipment-pkg-302: pickup at the warehouse (index 1), delivery to a customer in Schiedam (index 3). Demand {"weight": 25}.
  • shipment-pkg-303: pickup at a cold-storage facility (index 4), delivery to a restaurant (index 5). Requires required_skills: ["refrigerated"] because the cargo needs a refrigerated van. Demand {"weight": 15}.

Only van-9 has the refrigerated skill, so shipment-pkg-303 must go to it regardless of routing cost.

Request

{
  "metadata": { "problem_id": "pickup-delivery-example" },
  "travel": {
    "distance_matrix": [
      [0, 2100, 5400, 8200, 4100, 6700],
      [2100, 0, 4300, 7100, 3900, 6200],
      [5400, 4300, 0, 5800, 6700, 8900],
      [8200, 7100, 5800, 0, 9100, 11200],
      [4100, 3900, 6700, 9100, 0, 2600],
      [6700, 6200, 8900, 11200, 2600, 0]
    ]
  },
  "resources": [
    { "id": "van-7", "depot_index": 0, "capacities": { "weight": 200 } },
    { "id": "van-9", "depot_index": 0, "capacities": { "weight": 200 }, "skills": ["refrigerated"] }
  ],
  "shipments": [
    {
      "id": "shipment-pkg-301",
      "pickup": { "id": "shipment-pkg-301-pickup", "location_index": 1, "service_time": 300, "demands": { "weight": 40 } },
      "delivery": { "id": "shipment-pkg-301-delivery", "location_index": 2, "service_time": 240, "demands": { "weight": 40 } }
    },
    {
      "id": "shipment-pkg-302",
      "pickup": { "id": "shipment-pkg-302-pickup", "location_index": 1, "service_time": 300, "demands": { "weight": 25 } },
      "delivery": { "id": "shipment-pkg-302-delivery", "location_index": 3, "service_time": 240, "demands": { "weight": 25 } }
    },
    {
      "id": "shipment-pkg-303",
      "pickup": { "id": "shipment-pkg-303-pickup", "location_index": 4, "service_time": 360, "demands": { "weight": 15 } },
      "delivery": { "id": "shipment-pkg-303-delivery", "location_index": 5, "service_time": 240, "demands": { "weight": 15 } },
      "required_skills": ["refrigerated"]
    }
  ],
  "options": { "time_limit_seconds": 5 }
}

Response

{
  "status": "optimal",
  "problem_id": "pickup-delivery-example",
  "solve_time_ms": 47,
  "routes": [
    {
      "resource_id": "van-7",
      "day": null,
      "activities": [
        { "type": "start", "location_index": 0, "departure_time": 0 },
        { "type": "pickup", "shipment_id": "shipment-pkg-301", "location_index": 1, "arrival_time": 2100, "departure_time": 2400, "service_time": 300, "load_on_departure": { "total": 65, "by_dimension": { "weight": 65 } }, "distance_from_prev": 2100, "time_from_prev": 2100 },
        { "type": "pickup", "shipment_id": "shipment-pkg-302", "location_index": 1, "arrival_time": 2400, "departure_time": 2400, "service_time": 0, "load_on_departure": { "total": 65, "by_dimension": { "weight": 65 } }, "distance_from_prev": 0, "time_from_prev": 0 },
        { "type": "delivery", "shipment_id": "shipment-pkg-301", "location_index": 2, "arrival_time": 6700, "departure_time": 6940, "service_time": 240, "load_on_departure": { "total": 25, "by_dimension": { "weight": 25 } }, "distance_from_prev": 4300, "time_from_prev": 4300 },
        { "type": "delivery", "shipment_id": "shipment-pkg-302", "location_index": 3, "arrival_time": 12740, "departure_time": 12980, "service_time": 240, "load_on_departure": { "total": 0, "by_dimension": { "weight": 0 } }, "distance_from_prev": 5800, "time_from_prev": 5800 },
        { "type": "end", "location_index": 0, "arrival_time": 21180, "distance_from_prev": 8200, "time_from_prev": 8200 }
      ],
      "summary": { "total_distance": 20400, "total_travel_time": 20400, "total_service_time": 780, "total_waiting_time": 0, "total_time": 21180, "num_stops": 4, "num_jobs": 0, "max_loads": { "weight": 65 }, "cost": 20400.0 },
      "violations": []
    },
    {
      "resource_id": "van-9",
      "day": null,
      "activities": [
        { "type": "start", "location_index": 0, "departure_time": 0 },
        { "type": "pickup", "shipment_id": "shipment-pkg-303", "location_index": 4, "arrival_time": 4100, "departure_time": 4460, "service_time": 360, "load_on_departure": { "total": 15, "by_dimension": { "weight": 15 } }, "distance_from_prev": 4100, "time_from_prev": 4100 },
        { "type": "delivery", "shipment_id": "shipment-pkg-303", "location_index": 5, "arrival_time": 7060, "departure_time": 7300, "service_time": 240, "load_on_departure": { "total": 0, "by_dimension": { "weight": 0 } }, "distance_from_prev": 2600, "time_from_prev": 2600 },
        { "type": "end", "location_index": 0, "arrival_time": 14000, "distance_from_prev": 6700, "time_from_prev": 6700 }
      ],
      "summary": { "total_distance": 13400, "total_travel_time": 13400, "total_service_time": 600, "total_waiting_time": 0, "total_time": 14000, "num_stops": 2, "num_jobs": 0, "max_loads": { "weight": 15 }, "cost": 13400.0 },
      "violations": []
    }
  ],
  "unserved": [],
  "score": { "total": 33800.0, "travel_distance": 33800.0, "travel_time": 33800.0, "fixed_vehicle_cost": 0.0, "penalty_dropped": 0.0, "fairness_cost": 0.0, "num_vehicles_used": 2 },
  "warnings": []
}

The pickup-before-delivery guarantee

Notice in van-7's route that both pickups (shipment-pkg-301 and shipment-pkg-302, sharing location index 1, the warehouse) happen before either delivery. The solver is free to batch multiple pickups together — it is not required to immediately deliver after each pickup — but it can never schedule a delivery activity for a shipment before that shipment's pickup activity, and never splits a shipment's two legs across different resources. load_on_departure tracks the running total: it climbs to 65 (40 + 25) after both pickups, drops to 25 after delivering shipment-pkg-301, and returns to 0 after delivering shipment-pkg-302.

shipment-pkg-303 went to van-9 not because it was cheaper (van-7 is actually slightly closer to the cold-storage pickup) but because required_skills: ["refrigerated"] filters out any vehicle that lacks the refrigerated skill — van-7 was never eligible.

Activities have no job_id for shipments

Shipment-related activities set shipment_id instead of job_id, and the route summary's num_jobs only counts plain jobs entries — shipments are tracked separately via num_stops.

Next steps

FieldGenius VRP API documentation. Generated from the engineering source of truth (.tex docs and app/ source).