Calculate custom costs per job
The costPerJob feature enables charging a cost per specific job instead having to rely only on time, distance, or fixed costs. This capability lets you cover a wide variety of business models, such as:
- Parcel/weight-based variable pricing. For example, when delivering heavier or multi-box orders incurs a higher handling fee.
- Delivery Service Partner (DSP) rate cards. For example, outsourced contractors might charge different flat rates per stop.
- Driver-customer affinity (soft preference for familiar drivers). For example, when customers prefer seeing the same driver regularly, but other drivers can serve them if routes become too inefficient.
NoteThis is an ALPHA feature. To enable it, add
costPerJobtoexperimentalFeaturesin the problem configuration.For more information, see Explore experimental features.
Define per-job cost rate and cost factor
You set up job-specific costs in the following problem components:
-
costs.perJob.rateon a vehicle type. This is the base rate that represents the "fixed" cost that the optimization algorithm applies to all jobs that vehicles of this type complete. The following example shows how to configure theperJob.rateparameter in a vehicle type:{ "costs": { "time": 0.005, "perJob": { "rate": 5 } } } -
plan.jobs[].costFactor. Represents a multiplier that corresponds to the "size" or "complexity" of a job from your business perspective. For example, you might set a higher cost factor if a job requires delivering heavy goods, involves handling multiple packages, and so on. If not specified, the default value is1. The following example shows how to specifycostFactorfor an individual job:{ "id": "Delivery_1", "costFactor": 4, "tasks": { "deliveries": [ { "places": [ { "duration": 300, "location": { "lat": 52.5299, "lng": 13.401 } } ], "demand": [ 4 ] } ] } }
Calculate the per-job cost
The API calculates each job's cost by multiplying the job's costFactor by the vehicle type's costs.perJob.rate.
Per-job costcostPerJob = costFactor × rateFor example, a job with
costFactor: 4andrate: 5has a per-job cost of4 × 5 = 20.
The solution returns the sum of all per-job costs as the perJob property within the statistic.costs object at the general solution level and for each individual tour. The following example shows the perJob property in the solution's statistic.costs object:
{
"statistic": {
"costs": {
"distance": 5.49585,
"perJob": 45,
"time": 14.095
}
}
}If you specify costFactor without configuring the perJob.rate parameter in a problem, the API triggers the costFactorWithoutPerJob notice, as shown in the following example:
"notices": [
{
"code": "costFactorWithoutPerJob",
"title": "Jobs have costFactor set but no vehicle defines perJob cost: Delivery_1, Delivery_3",
"action": "Add perJob to vehicle costs or remove costFactor from jobs"
}
]For more information, see API Reference.
Override the cost for specific jobs
The API also provides the costs.perJob.overrides[] parameter for more flexibility. By using this parameter you can ignore costFactor and perJob.rate settings for specific jobs when your business model requires a specific cost for certain known jobs regardless of their complexity or other factors.
For example, a rush delivery job might require priority handling with fixed $20 pricing, bypassing the normal per-job rate calculations.
The following example shows how to override the cost for specific job IDs:
{
"costs": {
"time": 0.005,
"perJob": {
"rate": 5,
"overrides": [
{
"jobId": "RushDelivery",
"cost": 20
}
]
}
}
}In this example, RushDelivery is assigned a flat cost of 20. This override bypasses both the vehicle's normal perJob.rate and any costFactor on the job.
Example: Assigning jobs to vehicles when capacity is limited
This sample scenario shows how per-job costs influence the following tour aspects:
- Which vehicle type serves which job when the vehicle types have different per-job rates.
- Which jobs the algorithm leaves unassigned when the fleet capacity is not enough to serve all of them.
The example problem consists of the following key components:
- Fleet:
- vehicle type
id:vehicle_standard- vehicles available:
1 - capacity:
4 perJob.rate:5
- vehicles available:
- vehicle type
id:vehicle_premium- vehicles available:
1 - capacity:
4 perJob.rate:15
- vehicles available:
- Both vehicle types start and end their shifts at the same depot and have identical time and distance costs. They differ only in the
perJob.ratevalue.
- vehicle type
- Jobs: 10 deliveries, each with a demand of
1and its owncostFactor, which reflects the complexity of the job.
The following table shows the cost of each job for each vehicle type, calculated as costFactor × rate:
| Job ID | costFactor | vehicle_standard (rate: 5) | vehicle_premium (rate: 15) |
|---|---|---|---|
Job_CF1_A | 1 | 5 | 15 |
Job_CF1_B | 1 | 5 | 15 |
Job_CF2_A | 2 | 10 | 30 |
Job_CF2_B | 2 | 10 | 30 |
Job_CF5_A | 5 | 25 | 75 |
Job_CF5_B | 5 | 25 | 75 |
Job_CF6_A | 6 | 30 | 90 |
Job_CF6_B | 6 | 30 | 90 |
Job_CF10_A | 10 | 50 | 150 |
Job_CF10_B | 10 | 50 | 150 |
NoteConsider the following aspects of this tour setup:
vehicle_standardis cheaper for every job, but it can serve only four of them. The more complex a job is, the more you save by assigning it tovehicle_standard.Job_CF10_AandJob_CF10_Bare the closest jobs to the depot (within 1 km), but they have the highestcostFactor.- The total demand is
10, but the two vehicles can carry only8units together (2 × 4). The algorithm cannot serve all jobs and needs to decide which two to leave out.
The following figure visualizes the problem:
The following section shows the full problem configuration:
Click to expand/collapse the sample JSON
{
"configuration": {
"experimentalFeatures": [
"costPerJob"
]
},
"plan": {
"jobs": [
{
"id": "Job_CF6_A",
"costFactor": 6,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.56,
"lng": 13.33
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Job_CF1_A",
"costFactor": 1,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.575,
"lng": 13.36
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Job_CF2_A",
"costFactor": 2,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.49,
"lng": 13.35
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Job_CF5_A",
"costFactor": 5,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.48,
"lng": 13.4
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Job_CF1_B",
"costFactor": 1,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.565,
"lng": 13.4
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Job_CF6_B",
"costFactor": 6,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.58,
"lng": 13.39
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Job_CF2_B",
"costFactor": 2,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.47,
"lng": 13.37
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Job_CF5_B",
"costFactor": 5,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.495,
"lng": 13.43
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Job_CF10_A",
"costFactor": 10,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.533,
"lng": 13.38
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Job_CF10_B",
"costFactor": 10,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.531,
"lng": 13.37
}
}
],
"demand": [
1
]
}
]
}
}
]
},
"fleet": {
"types": [
{
"id": "vehicle_standard",
"profile": "vehicle",
"amount": 1,
"capacity": [
4
],
"costs": {
"fixed": 0,
"distance": 0.00045,
"time": 0.0041666667,
"perJob": {
"rate": 5
}
},
"shifts": [
{
"start": {
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": "2026-07-30T08:00:00+02:00"
},
"end": {
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": "2026-07-30T18:00:00+02:00"
}
}
]
},
{
"id": "vehicle_premium",
"profile": "vehicle",
"amount": 1,
"capacity": [
4
],
"costs": {
"fixed": 0,
"distance": 0.00045,
"time": 0.0041666667,
"perJob": {
"rate": 15
}
},
"shifts": [
{
"start": {
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": "2026-07-30T08:00:00+02:00"
},
"end": {
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": "2026-07-30T18:00:00+02:00"
}
}
]
}
],
"profiles": [
{
"name": "vehicle",
"type": "car"
}
]
}
}Solution interpretation
The solution contains two tours with four jobs each, and two unassigned jobs. The following figure visualizes the tours for each vehicle:
The following table shows how the optimization algorithm assigned the jobs:
| Tour | Jobs | costFactor values | Per-job cost |
|---|---|---|---|
vehicle_standard_1 | Job_CF6_A, Job_CF6_B, Job_CF5_A, Job_CF5_B | 6, 6, 5, 5 | 5 × 22 = 110 |
vehicle_premium_1 | Job_CF1_A, Job_CF1_B, Job_CF2_A, Job_CF2_B | 1, 1, 2, 2 | 15 × 6 = 90 |
The algorithm put the four jobs with the highest costFactor into the cheaper vehicle (vehicle_standard_1), because this is where the savings are the largest, while the four jobs with the lowest costFactor stay with vehicle_premium_1.
The most expensive Job_CF10_A and Job_CF10_B are unassigned with the CAPACITY_CONSTRAINT reason code, as the following example shows:
"unassigned": [
{
"jobId": "Job_CF10_A",
"reasons": [
{
"code": "CAPACITY_CONSTRAINT",
"description": "cannot be assigned due to capacity of vehicle"
}
]
}
]Serving Job_CF10_A or Job_CF10_B would be easy from the distance point of view, but each of them costs at least 50, while every assigned job costs at most 30 on the vehicle that serves it. For example, replacing Job_CF5_A (25) with Job_CF10_A (50) in the vehicle_standard_1 tour would raise the per-job cost by 25. This shows that when capacity is limited, the costFactor values can outweigh the distance savings of serving nearby jobs.
The following section provides the full solution JSON:
Click to expand/collapse the sample JSON
{
"statistic": {
"cost": 278.185750373,
"distance": 70135,
"duration": 11190,
"times": {
"break": 0,
"driving": 8790,
"serving": 2400,
"stopping": 0,
"waiting": 0
},
"costs": {
"distance": 31.56075,
"perJob": 200,
"time": 46.625000373000006
},
"intraStopDistance": 0
},
"tours": [
{
"shiftIndex": 0,
"statistic": {
"cost": 149.4116168525,
"distance": 35961,
"duration": 5575,
"times": {
"break": 0,
"driving": 4375,
"serving": 1200,
"stopping": 0,
"waiting": 0
},
"costs": {
"distance": 16.18245,
"perJob": 110,
"time": 23.2291668525
},
"intraStopDistance": 0
},
"stops": [
{
"activities": [
{
"jobId": "departure",
"type": "departure",
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": {
"arrival": "2026-07-30T06:00:00Z",
"start": "2026-07-30T06:00:00Z",
"end": "2026-07-30T06:00:00Z"
}
}
],
"load": [
4
],
"time": {
"arrival": "2026-07-30T06:00:00Z",
"departure": "2026-07-30T06:00:00Z"
},
"distance": 0,
"location": {
"lat": 52.5286,
"lng": 13.3764
}
},
{
"activities": [
{
"jobId": "Job_CF6_A",
"type": "delivery",
"location": {
"lat": 52.56,
"lng": 13.33
},
"time": {
"arrival": "2026-07-30T06:11:32Z",
"start": "2026-07-30T06:11:32Z",
"end": "2026-07-30T06:16:32Z"
}
}
],
"load": [
3
],
"time": {
"arrival": "2026-07-30T06:11:32Z",
"departure": "2026-07-30T06:16:32Z"
},
"distance": 5568,
"location": {
"lat": 52.56,
"lng": 13.33
}
},
{
"activities": [
{
"jobId": "Job_CF6_B",
"type": "delivery",
"location": {
"lat": 52.58,
"lng": 13.39
},
"time": {
"arrival": "2026-07-30T06:30:37Z",
"start": "2026-07-30T06:30:37Z",
"end": "2026-07-30T06:35:37Z"
}
}
],
"load": [
2
],
"time": {
"arrival": "2026-07-30T06:30:37Z",
"departure": "2026-07-30T06:35:37Z"
},
"distance": 12313,
"location": {
"lat": 52.58,
"lng": 13.39
}
},
{
"activities": [
{
"jobId": "Job_CF5_B",
"type": "delivery",
"location": {
"lat": 52.495,
"lng": 13.43
},
"time": {
"arrival": "2026-07-30T07:02:46Z",
"start": "2026-07-30T07:02:46Z",
"end": "2026-07-30T07:07:46Z"
}
}
],
"load": [
1
],
"time": {
"arrival": "2026-07-30T07:02:46Z",
"departure": "2026-07-30T07:07:46Z"
},
"distance": 25050,
"location": {
"lat": 52.495,
"lng": 13.43
}
},
{
"activities": [
{
"jobId": "Job_CF5_A",
"type": "delivery",
"location": {
"lat": 52.48,
"lng": 13.4
},
"time": {
"arrival": "2026-07-30T07:15:11Z",
"start": "2026-07-30T07:15:11Z",
"end": "2026-07-30T07:20:11Z"
}
}
],
"load": [
0
],
"time": {
"arrival": "2026-07-30T07:15:11Z",
"departure": "2026-07-30T07:20:11Z"
},
"distance": 28697,
"location": {
"lat": 52.48,
"lng": 13.4
}
},
{
"activities": [
{
"jobId": "arrival",
"type": "arrival",
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": {
"arrival": "2026-07-30T07:32:55Z",
"start": "2026-07-30T07:32:55Z",
"end": "2026-07-30T07:32:55Z"
}
}
],
"load": [
0
],
"time": {
"arrival": "2026-07-30T07:32:55Z",
"departure": "2026-07-30T07:32:55Z"
},
"distance": 35961,
"location": {
"lat": 52.5286,
"lng": 13.3764
}
}
],
"typeId": "vehicle_standard",
"vehicleId": "vehicle_standard_1"
},
{
"shiftIndex": 0,
"statistic": {
"cost": 128.77413352050002,
"distance": 34174,
"duration": 5615,
"times": {
"break": 0,
"driving": 4415,
"serving": 1200,
"stopping": 0,
"waiting": 0
},
"costs": {
"distance": 15.3783,
"perJob": 90,
"time": 23.395833520500002
},
"intraStopDistance": 0
},
"stops": [
{
"activities": [
{
"jobId": "departure",
"type": "departure",
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": {
"arrival": "2026-07-30T06:00:00Z",
"start": "2026-07-30T06:00:00Z",
"end": "2026-07-30T06:00:00Z"
}
}
],
"load": [
4
],
"time": {
"arrival": "2026-07-30T06:00:00Z",
"departure": "2026-07-30T06:00:00Z"
},
"distance": 0,
"location": {
"lat": 52.5286,
"lng": 13.3764
}
},
{
"activities": [
{
"jobId": "Job_CF1_A",
"type": "delivery",
"location": {
"lat": 52.575,
"lng": 13.36
},
"time": {
"arrival": "2026-07-30T06:12:18Z",
"start": "2026-07-30T06:12:18Z",
"end": "2026-07-30T06:17:18Z"
}
}
],
"load": [
3
],
"time": {
"arrival": "2026-07-30T06:12:18Z",
"departure": "2026-07-30T06:17:18Z"
},
"distance": 5942,
"location": {
"lat": 52.575,
"lng": 13.36
}
},
{
"activities": [
{
"jobId": "Job_CF1_B",
"type": "delivery",
"location": {
"lat": 52.565,
"lng": 13.4
},
"time": {
"arrival": "2026-07-30T06:27:24Z",
"start": "2026-07-30T06:27:24Z",
"end": "2026-07-30T06:32:24Z"
}
}
],
"load": [
2
],
"time": {
"arrival": "2026-07-30T06:27:24Z",
"departure": "2026-07-30T06:32:24Z"
},
"distance": 10139,
"location": {
"lat": 52.565,
"lng": 13.4
}
},
{
"activities": [
{
"jobId": "Job_CF2_A",
"type": "delivery",
"location": {
"lat": 52.49,
"lng": 13.35
},
"time": {
"arrival": "2026-07-30T06:56:30Z",
"start": "2026-07-30T06:56:30Z",
"end": "2026-07-30T07:01:30Z"
}
}
],
"load": [
1
],
"time": {
"arrival": "2026-07-30T06:56:30Z",
"departure": "2026-07-30T07:01:30Z"
},
"distance": 21319,
"location": {
"lat": 52.49,
"lng": 13.35
}
},
{
"activities": [
{
"jobId": "Job_CF2_B",
"type": "delivery",
"location": {
"lat": 52.47,
"lng": 13.37
},
"time": {
"arrival": "2026-07-30T07:08:31Z",
"start": "2026-07-30T07:08:31Z",
"end": "2026-07-30T07:13:31Z"
}
}
],
"load": [
0
],
"time": {
"arrival": "2026-07-30T07:08:31Z",
"departure": "2026-07-30T07:13:31Z"
},
"distance": 24678,
"location": {
"lat": 52.47,
"lng": 13.37
}
},
{
"activities": [
{
"jobId": "arrival",
"type": "arrival",
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": {
"arrival": "2026-07-30T07:33:35Z",
"start": "2026-07-30T07:33:35Z",
"end": "2026-07-30T07:33:35Z"
}
}
],
"load": [
0
],
"time": {
"arrival": "2026-07-30T07:33:35Z",
"departure": "2026-07-30T07:33:35Z"
},
"distance": 34174,
"location": {
"lat": 52.5286,
"lng": 13.3764
}
}
],
"typeId": "vehicle_premium",
"vehicleId": "vehicle_premium_1"
}
],
"unassigned": [
{
"jobId": "Job_CF10_A",
"reasons": [
{
"code": "CAPACITY_CONSTRAINT",
"description": "cannot be assigned due to capacity of vehicle"
}
]
},
{
"jobId": "Job_CF10_B",
"reasons": [
{
"code": "CAPACITY_CONSTRAINT",
"description": "cannot be assigned due to capacity of vehicle"
}
]
}
]
}Next steps
- For information on other cost-related features you can use, see:
- For an in-depth exploration of the HERE Tour Planning API methods, endpoints, and parameters, see the API Reference.
Updated 9 days ago