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:
| Cap | Catches |
|---|---|
max_payload_kg | Dense heavy loads (sum of actual weights) |
max_volumetric_kg | Bulky light loads (sum of volumetric weights — (L × W × H) / volumetric_divisor) |
max_length_cm | Single 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,
...
}
]
| Field | Type | Description |
|---|---|---|
id | integer | Use this as vehicle_type_id to override auto-selection |
name | string | Display name |
slug | string | URL-friendly identifier (motorcycle, bicycle, car, van, truck, guta, fuso, foot) |
description | string | Short marketing description |
icon | string | Icon identifier for your UI |
base_fee | integer | Fixed base fee in TZS |
price_per_km | integer | Per-kilometer rate in TZS |
minimum_fee | integer | Minimum delivery fee in TZS |
max_payload_kg | number | null | Mass cap — sum of actual weights (null = excluded from auto-selection) |
max_volumetric_kg | number | null | Volume cap — sum of volumetric weights (null = excluded) |
max_length_cm | number | null | Single-item longest-side cap (null = excluded) |
max_distance_km | number | null | Maximum delivery distance (null = no limit) |
is_active | boolean | Currently available — false types are hidden from this list |
sort_order | integer | Display order |
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.
| Vehicle | Use case |
|---|---|
| On Foot | Hyperlocal (< 1 km) |
| Bicycle | Documents, very small items |
| Motorcycle | Food, small parcels (default for most shipments) |
| Guta (Bajaj) | Multiple containers, medium loads |
| Car | Larger packages |
| Van | Bulk, furniture |
| Truck | Heavy cargo |
| Fuso | Large 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.