Skip to main content

Vehicle Types

Vehicles determine pricing tiers and what kinds of items can be moved. You normally don't pick a vehicle yourself — the rate engine auto-selects based on item weight and dimensions. This page is for cases where you need to surface choices to your customer or override the selection.

How vehicle selection works

When you call POST /merchant/shipments/rates or POST /merchant/shipments, the engine performs a three-axis eligibility check across all items in your shipment:

CapCatches
max_payload_kgDense heavy loads (sum of actual weights)
max_volumetric_kgBulky light loads (sum of volumetric weights — (L × W × H) / volumetric_divisor)
max_length_cmSingle long items (longest side of any item)

A vehicle is eligible only when all three caps pass:

sum(item.actual_weight × qty)      ≤ vehicle.max_payload_kg
sum(item.volumetric_weight × qty) ≤ vehicle.max_volumetric_kg
max over items of max(L, W, H) ≤ vehicle.max_length_cm

The engine picks the smallest eligible vehicle by max_payload_kg. The chosen vehicle_id and vehicle_name come back in the response.

If nothing fits, you'll get a 400 with the specific axis that breached — there is no silent fallback to the largest vehicle. Split the shipment, drop the offending item, or use a smaller tier.

Overriding

Pass vehicle_type_id on the request to force a specific vehicle. Use this when:

  • You want to force a larger vehicle (e.g. awkward but light cargo)
  • You're letting the end customer choose ("Send by bicycle / motorcycle / car")

Overrides go through the same three-axis check. An undersized override returns 400, not a silent upgrade.

List vehicle types

GET /vehicle-types

Public endpoint — no authentication required. Returns all active vehicle types sorted by display order.

curl https://api.dodo.co.tz/api/v1/vehicle-types

Response (200 OK):

[
{
"id": 1,
"name": "Motorcycle",
"slug": "motorcycle",
"description": "Fast delivery for small packages",
"icon": "motorcycle",
"base_fee": 1500,
"price_per_km": 500,
"minimum_fee": 1500,
"max_payload_kg": 20,
"max_volumetric_kg": 15,
"max_length_cm": 90,
"max_distance_km": null,
"is_active": true,
"sort_order": 1
},
{
"id": 2,
"name": "Bicycle",
"slug": "bicycle",
"base_fee": 500,
"price_per_km": 200,
"minimum_fee": 500,
"max_payload_kg": 10,
"max_volumetric_kg": 8,
"max_length_cm": 60,
...
}
]
FieldTypeDescription
idintegerUse this as vehicle_type_id to override auto-selection
namestringDisplay name
slugstringURL-friendly identifier (motorcycle, bicycle, car, van, truck, guta, fuso, foot)
descriptionstringShort marketing description
iconstringIcon identifier for your UI
base_feeintegerFixed base fee in TZS
price_per_kmintegerPer-kilometer rate in TZS
minimum_feeintegerMinimum delivery fee in TZS
max_payload_kgnumber | nullMass cap — sum of actual weights (null = excluded from auto-selection)
max_volumetric_kgnumber | nullVolume cap — sum of volumetric weights (null = excluded)
max_length_cmnumber | nullSingle-item longest-side cap (null = excluded)
max_distance_kmnumber | nullMaximum delivery distance (null = no limit)
is_activebooleanCurrently available — false types are hidden from this list
sort_orderintegerDisplay order
Vehicles with NULL caps are excluded

A vehicle row with null on any capacity axis is invisible to auto-selection until an admin fills it in. If you override into such a vehicle with vehicle_type_id, you'll still get the same 400 unless the override happens to fit every non-null axis.

Vehicle types in service

Every vehicle below is a separate row in GET /vehicle-types with its own base / per-km / minimum and three capacity caps. Use cases are guidance — the engine picks based purely on the load math.

VehicleUse case
On FootHyperlocal (< 1 km)
BicycleDocuments, very small items
MotorcycleFood, small parcels (default for most shipments)
Guta (Bajaj)Multiple containers, medium loads
CarLarger packages
VanBulk, furniture
TruckHeavy cargo
FusoLarge freight

Always fetch live pricing and caps from GET /vehicle-types. Don't hardcode rates on your side — admins can retune them without warning.

Pricing formula (same-city)

fee = max(base_fee + distance_km × price_per_km, minimum_fee)

A platform service fee is added on top (currently 10%). See Rate Quotes for the full breakdown including fragile surcharges.

Letting your customer choose

If you want to surface vehicle options in your UI:

// Fetch once at app load — cache in memory
const vehicles = await fetch(
'https://api.dodo.co.tz/api/v1/vehicle-types'
).then(r => r.json());

// Filter to whatever you want to offer
const choices = vehicles.filter(v => ['bicycle', 'motorcycle', 'car'].includes(v.slug));

// For each option, get a quote
for (const v of choices) {
const quote = await fetch('/api/v1/merchant/shipments/rates', {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
pickup_latitude: -6.79,
pickup_longitude: 39.21,
dropoff_latitude: -6.76,
dropoff_longitude: 39.24,
items: [{
description: 'parcel',
weight_kg: 3, length_cm: 25, width_cm: 20, height_cm: 15,
quantity: 1,
}],
vehicle_type_id: v.id, // force this vehicle
}),
}).then(r => r.json());

console.log(`${v.name}: ${quote.total_amount} TZS`);
}

Caching

Vehicle types change rarely. Cache GET /vehicle-types results for at least an hour on your side — don't fetch them on every quote request.