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.
Note
This 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 cost
costPerJob = 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_Premium_001",
"cost": 20
}
]
}
}
}In this example, RushDelivery_Premium_001 is assigned a flat cost of 20. This override bypasses both the vehicle's normal perJob.rate and any costFactor on the job.
Example: Choosing the cheaper vehicle despite a longer drive
The following sections walk you through a practical example that demonstrates how per-job costs differences might impact tour planning. This example is a problem that consists of the following key components:
- Fleet:
- vehicle type
id:vehicle_premium:- vehicles available:
1 - located in central Berlin
- the
perJob.ratevalue is15
- vehicles available:
- vehicle type
id:vehicle_standard- located in south Berlin
- vehicles available:
1 - the
perJob.ratevalue is5
- Jobs:
Delivery_1:costFactor: 2Delivery_2: nocostFactor, defaults to1RushDelivery_Premium_001: both vehicle types override this job's cost by setting it to 20 flatDelivery_3:costFactor: 2
- vehicle type
The important factor to consider in this use case is that the depot for vehicle_premium is significantly closer to most delivery locations, as shown on the following map:
Together with a 3x higher per-job rate than vehicle_standard, this problem setup creates an interesting challenge for the optimization algorithm, in which it needs to find the right balance between travel efficiency and per-job cost.
Baseline: Routing with identical per-job rates
In this baseline scenario, both vehicle types have the same costPerJob settings (perJob.rate: 5). This leaves the distance between the depots and the job locations as the most important factor in the process of determining the right vehicle type for the job. Therefore, in this case, the vehicle type whose depot is closer is likely to be selected by the optimization algorithm.
See the following section for the full problem configuration:
Click to expand/collapse the sample JSON
{
"configuration": {
"experimentalFeatures": [
"costPerJob"
]
},
"fleet": {
"types": [
{
"id": "vehicle_premium",
"profile": "vehicle",
"amount": 1,
"capacity": [
20
],
"costs": {
"fixed": 0,
"distance": 0.00045,
"time": 0.005,
"perJob": {
"rate": 5,
"overrides": [
{
"jobId": "RushDelivery_Premium_001",
"cost": 20
}
]
}
},
"shifts": [
{
"start": {
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": "2026-07-30T08:00:00+05:30"
},
"end": {
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": "2026-07-30T18:00:00+05:30"
}
}
]
},
{
"id": "vehicle_standard",
"profile": "vehicle",
"amount": 1,
"capacity": [
30
],
"costs": {
"fixed": 0,
"distance": 0.00045,
"time": 0.005,
"perJob": {
"rate": 5,
"overrides": [
{
"jobId": "RushDelivery_Premium_001",
"cost": 20
}
]
}
},
"shifts": [
{
"start": {
"location": {
"lat": 52.46764,
"lng": 13.383
},
"time": "2026-07-30T08:00:00+05:30"
},
"end": {
"location": {
"lat": 52.46764,
"lng": 13.383
},
"time": "2026-07-30T18:00:00+05:30"
}
}
]
}
],
"profiles": [
{
"name": "vehicle",
"type": "car"
}
]
},
"plan": {
"jobs": [
{
"id": "Delivery_1",
"costFactor": 2,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.5299,
"lng": 13.401
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Delivery_2",
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.5219,
"lng": 13.4132
}
}
],
"demand": [
2
]
}
]
}
},
{
"id": "RushDelivery_Premium_001",
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.5096,
"lng": 13.376
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Delivery_3",
"costFactor": 2,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.52426,
"lng": 13.35372
}
}
],
"demand": [
1
]
}
]
}
}
]
}
}Solution interpretation - baseline scenario
As predicted, with identical perJob.rate values, the vehicle_premium type got selected due to its proximity to job locations that resulted in more efficient distance-related costs, as the following visualization shows:
Note
Removing the
vehicle_premiumvehicle type from the problem makes the optimization algorithm select the only remaining vehicle type (vehicle_standard) with identicalperJobcosts. For more information about testing Tour Planning problems on your own, see Try requests in the Tour Planning API demo tool.
See the following section for the full solution JSON, including the perJob value within the statistic.costs object:
Click to expand/collapse the sample JSON
{
"statistic": {
"cost": 64.59085,
"distance": 12213,
"duration": 2819,
"times": {
"break": 0,
"driving": 1619,
"serving": 1200,
"stopping": 0,
"waiting": 0
},
"costs": {
"distance": 5.49585,
"perJob": 45,
"time": 14.095
},
"intraStopDistance": 0
},
"tours": [
{
"shiftIndex": 0,
"statistic": {
"cost": 64.59085,
"distance": 12213,
"duration": 2819,
"times": {
"break": 0,
"driving": 1619,
"serving": 1200,
"stopping": 0,
"waiting": 0
},
"costs": {
"distance": 5.49585,
"perJob": 45,
"time": 14.095
},
"intraStopDistance": 0
},
"stops": [
{
"activities": [
{
"jobId": "departure",
"type": "departure",
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": {
"arrival": "2026-07-30T02:30:00Z",
"start": "2026-07-30T02:30:00Z",
"end": "2026-07-30T02:30:00Z"
}
}
],
"load": [
5
],
"time": {
"arrival": "2026-07-30T02:30:00Z",
"departure": "2026-07-30T02:30:00Z"
},
"distance": 0,
"location": {
"lat": 52.5286,
"lng": 13.3764
}
},
{
"activities": [
{
"jobId": "Delivery_1",
"type": "delivery",
"location": {
"lat": 52.5299,
"lng": 13.401
},
"time": {
"arrival": "2026-07-30T02:34:12Z",
"start": "2026-07-30T02:34:12Z",
"end": "2026-07-30T02:39:12Z"
}
}
],
"load": [
4
],
"time": {
"arrival": "2026-07-30T02:34:12Z",
"departure": "2026-07-30T02:39:12Z"
},
"distance": 1956,
"location": {
"lat": 52.5299,
"lng": 13.401
}
},
{
"activities": [
{
"jobId": "Delivery_2",
"type": "delivery",
"location": {
"lat": 52.5219,
"lng": 13.4132
},
"time": {
"arrival": "2026-07-30T02:42:59Z",
"start": "2026-07-30T02:42:59Z",
"end": "2026-07-30T02:47:59Z"
}
}
],
"load": [
2
],
"time": {
"arrival": "2026-07-30T02:42:59Z",
"departure": "2026-07-30T02:47:59Z"
},
"distance": 3328,
"location": {
"lat": 52.5219,
"lng": 13.4132
}
},
{
"activities": [
{
"jobId": "RushDelivery_Premium_001",
"type": "delivery",
"location": {
"lat": 52.5096,
"lng": 13.376
},
"time": {
"arrival": "2026-07-30T02:55:27Z",
"start": "2026-07-30T02:55:27Z",
"end": "2026-07-30T03:00:27Z"
}
}
],
"load": [
1
],
"time": {
"arrival": "2026-07-30T02:55:27Z",
"departure": "2026-07-30T03:00:27Z"
},
"distance": 6785,
"location": {
"lat": 52.5096,
"lng": 13.376
}
},
{
"activities": [
{
"jobId": "Delivery_3",
"type": "delivery",
"location": {
"lat": 52.52426,
"lng": 13.35372
},
"time": {
"arrival": "2026-07-30T03:06:55Z",
"start": "2026-07-30T03:06:55Z",
"end": "2026-07-30T03:11:55Z"
}
}
],
"load": [
0
],
"time": {
"arrival": "2026-07-30T03:06:55Z",
"departure": "2026-07-30T03:11:55Z"
},
"distance": 10130,
"location": {
"lat": 52.52426,
"lng": 13.35372
}
},
{
"activities": [
{
"jobId": "arrival",
"type": "arrival",
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": {
"arrival": "2026-07-30T03:16:59Z",
"start": "2026-07-30T03:16:59Z",
"end": "2026-07-30T03:16:59Z"
}
}
],
"load": [
0
],
"time": {
"arrival": "2026-07-30T03:16:59Z",
"departure": "2026-07-30T03:16:59Z"
},
"distance": 12213,
"location": {
"lat": 52.5286,
"lng": 13.3764
}
}
],
"typeId": "vehicle_premium",
"vehicleId": "vehicle_premium_1"
}
]
}With higher Vehicle Premium rates
In this variant, perJob.rate for vehicle_premium increases from 5 to 15 making all jobs more expansive when using the premium vehicle. The following table demonstrates the expected outcome when using the premium vehicle with a 3x increased rate:
| Job ID | Cost Factor | Standard (Rate: $5) | Premium (Rate: $15) | Cost Difference |
|---|---|---|---|---|
Delivery_1 | 2 | 10 | 30 | +20 |
Delivery_2 | 1 | 5 | 15 | +10 |
Delivery_3 | 2 | 10 | 30 | +20 |
RushDelivery_Premium_001 | N/A (override) | 20 | 20 | $0 |
| Total | — | 45 | 95 | +50 |
With the premium rate, all jobs become significantly more expensive, especially the ones with a higher cost factors (Delivery_1 and Delivery_3), while the overridden rush delivery maintains its fixed cost. This cost gap between premium and standard rates creates a stronger incentive for the optimization algorithm to assign jobs to the standard vehicle.
The following section contains the full updated problem JSON:
Click to expand/collapse the sample JSON
{
"configuration": {
"experimentalFeatures": [
"costPerJob"
]
},
"fleet": {
"types": [
{
"id": "vehicle_premium",
"profile": "vehicle",
"amount": 1,
"capacity": [
20
],
"costs": {
"fixed": 0,
"distance": 0.00045,
"time": 0.005,
"perJob": {
"rate": 15,
"overrides": [
{
"jobId": "RushDelivery_Premium_001",
"cost": 20
}
]
}
},
"shifts": [
{
"start": {
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": "2026-07-30T08:00:00+05:30"
},
"end": {
"location": {
"lat": 52.5286,
"lng": 13.3764
},
"time": "2026-07-30T18:00:00+05:30"
}
}
]
},
{
"id": "vehicle_standard",
"profile": "vehicle",
"amount": 1,
"capacity": [
30
],
"costs": {
"fixed": 0,
"distance": 0.00045,
"time": 0.005,
"perJob": {
"rate": 5,
"overrides": [
{
"jobId": "RushDelivery_Premium_001",
"cost": 20
}
]
}
},
"shifts": [
{
"start": {
"location": {
"lat": 52.46764,
"lng": 13.383
},
"time": "2026-07-30T08:00:00+05:30"
},
"end": {
"location": {
"lat": 52.46764,
"lng": 13.383
},
"time": "2026-07-30T18:00:00+05:30"
}
}
]
}
],
"profiles": [
{
"name": "vehicle",
"type": "car"
}
]
},
"plan": {
"jobs": [
{
"id": "Delivery_1",
"costFactor": 2,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.5299,
"lng": 13.401
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Delivery_2",
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.5219,
"lng": 13.4132
}
}
],
"demand": [
2
]
}
]
}
},
{
"id": "RushDelivery_Premium_001",
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.5096,
"lng": 13.376
}
}
],
"demand": [
1
]
}
]
}
},
{
"id": "Delivery_3",
"costFactor": 2,
"tasks": {
"deliveries": [
{
"places": [
{
"duration": 300,
"location": {
"lat": 52.52426,
"lng": 13.35372
}
}
],
"demand": [
1
]
}
]
}
}
]
}
}Solution interpretation - higher premium rate
This time the optimization algorithm picked the standard vehicle for the tour, as the following visualization shows:
With the increased premium rates, vehicle_standard became the more efficient vehicle type of the two, despite the total tour distance being almost twice as much as for the premium vehicle (21,67 km vs 12,21 km).
The following table explains why the optimization algorithm switched to vehicle_standard. The extra distance costs the standard vehicle incurs are much smaller than the per-job savings it gains based on the lower perJob.rate.
| Category | vehicle_standard | vehicle_premium | Difference |
|---|---|---|---|
| Distance | 21.67 km | 12.21 km | +9.46 km |
| Distance cost | 9.75 | 5.50 | +4.25 |
| Per-job cost | 45 | 95 | -50 |
| Cost impact | — | — | -45.75 |
This data confirms that the vehicle type switch is driven purely by the perJob.rate difference and highlights the importance of accurately estimating costs of individual jobs in your business use case.
The following section provides the full solution JSON:
Click to expand/collapse the sample JSON
{
"statistic": {
"cost": 74.7556,
"distance": 21668,
"duration": 4001,
"times": {
"break": 0,
"driving": 2801,
"serving": 1200,
"stopping": 0,
"waiting": 0
},
"costs": {
"distance": 9.7506,
"perJob": 45,
"time": 20.005
},
"intraStopDistance": 0
},
"tours": [
{
"shiftIndex": 0,
"statistic": {
"cost": 74.7556,
"distance": 21668,
"duration": 4001,
"times": {
"break": 0,
"driving": 2801,
"serving": 1200,
"stopping": 0,
"waiting": 0
},
"costs": {
"distance": 9.7506,
"perJob": 45,
"time": 20.005
},
"intraStopDistance": 0
},
"stops": [
{
"activities": [
{
"jobId": "departure",
"type": "departure",
"location": {
"lat": 52.46764,
"lng": 13.383
},
"time": {
"arrival": "2026-07-30T02:30:00Z",
"start": "2026-07-30T02:30:00Z",
"end": "2026-07-30T02:30:00Z"
}
}
],
"load": [
5
],
"time": {
"arrival": "2026-07-30T02:30:00Z",
"departure": "2026-07-30T02:30:00Z"
},
"distance": 0,
"location": {
"lat": 52.46764,
"lng": 13.383
}
},
{
"activities": [
{
"jobId": "RushDelivery_Premium_001",
"type": "delivery",
"location": {
"lat": 52.5096,
"lng": 13.376
},
"time": {
"arrival": "2026-07-30T02:41:40Z",
"start": "2026-07-30T02:41:40Z",
"end": "2026-07-30T02:46:40Z"
}
}
],
"load": [
4
],
"time": {
"arrival": "2026-07-30T02:41:40Z",
"departure": "2026-07-30T02:46:40Z"
},
"distance": 5507,
"location": {
"lat": 52.5096,
"lng": 13.376
}
},
{
"activities": [
{
"jobId": "Delivery_3",
"type": "delivery",
"location": {
"lat": 52.52426,
"lng": 13.35372
},
"time": {
"arrival": "2026-07-30T02:53:08Z",
"start": "2026-07-30T02:53:08Z",
"end": "2026-07-30T02:58:08Z"
}
}
],
"load": [
3
],
"time": {
"arrival": "2026-07-30T02:53:08Z",
"departure": "2026-07-30T02:58:08Z"
},
"distance": 8852,
"location": {
"lat": 52.52426,
"lng": 13.35372
}
},
{
"activities": [
{
"jobId": "Delivery_1",
"type": "delivery",
"location": {
"lat": 52.5299,
"lng": 13.401
},
"time": {
"arrival": "2026-07-30T03:07:24Z",
"start": "2026-07-30T03:07:24Z",
"end": "2026-07-30T03:12:24Z"
}
}
],
"load": [
2
],
"time": {
"arrival": "2026-07-30T03:07:24Z",
"departure": "2026-07-30T03:12:24Z"
},
"distance": 12891,
"location": {
"lat": 52.5299,
"lng": 13.401
}
},
{
"activities": [
{
"jobId": "Delivery_2",
"type": "delivery",
"location": {
"lat": 52.5219,
"lng": 13.4132
},
"time": {
"arrival": "2026-07-30T03:16:11Z",
"start": "2026-07-30T03:16:11Z",
"end": "2026-07-30T03:21:11Z"
}
}
],
"load": [
0
],
"time": {
"arrival": "2026-07-30T03:16:11Z",
"departure": "2026-07-30T03:21:11Z"
},
"distance": 14263,
"location": {
"lat": 52.5219,
"lng": 13.4132
}
},
{
"activities": [
{
"jobId": "arrival",
"type": "arrival",
"location": {
"lat": 52.46764,
"lng": 13.383
},
"time": {
"arrival": "2026-07-30T03:36:41Z",
"start": "2026-07-30T03:36:41Z",
"end": "2026-07-30T03:36:41Z"
}
}
],
"load": [
0
],
"time": {
"arrival": "2026-07-30T03:36:41Z",
"departure": "2026-07-30T03:36:41Z"
},
"distance": 21668,
"location": {
"lat": 52.46764,
"lng": 13.383
}
}
],
"typeId": "vehicle_standard",
"vehicleId": "vehicle_standard_1"
}
]
}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 3 hours ago