Rate Quotes
Get a price estimate before creating a shipment. The rate endpoint resolves distance, picks a vehicle, and returns itemized pricing — without writing to the database or debiting your wallet.
Call this whenever you want to show a delivery cost to your end customer or check affordability before committing.
POST /merchant/shipments/rates
curl -X POST https://api.dodo.co.tz/api/v1/merchant/shipments/rates \
-H "X-API-Key: $DODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pickup_latitude": -6.7924,
"pickup_longitude": 39.2083,
"dropoff_latitude": -6.7600,
"dropoff_longitude": 39.2400,
"items": [
{
"description": "Food order",
"weight_kg": 2.5,
"quantity": 1,
"length_cm": 25,
"width_cm": 25,
"height_cm": 15
}
]
}'
Request
You must provide either GPS coordinates or an address string for each end of the route. Coordinates are strongly preferred — address strings are geocoded and may return imprecise locations.
| Field | Type | Required | Description |
|---|---|---|---|
pickup_latitude | number | conditional | Pickup latitude (-90 to 90) |
pickup_longitude | number | conditional | Pickup longitude (-180 to 180) |
pickup_address | string | conditional | Pickup address (used if coords omitted) |
dropoff_latitude | number | conditional | Dropoff latitude |
dropoff_longitude | number | conditional | Dropoff longitude |
dropoff_address | string | conditional | Dropoff address (used if coords omitted) |
items | array | yes | 1–50 items, see below |
vehicle_type_id | integer | no | Force a specific vehicle (otherwise auto-selected by weight) |
Item fields
Every shipment has at least one item. For same-city food/courier work, only description, weight_kg, and quantity are required. For regional or fragile items, supply dimensions so chargeable weight can be computed.
| Field | Type | Required | Description |
|---|---|---|---|
description | string | yes | What the item is (2–500 chars) |
weight_kg | number | yes | Actual weight in kilograms (0 < w ≤ 500) |
quantity | integer | yes | Number of units (1–100) |
length_cm | number | no | Length in centimeters |
width_cm | number | no | Width in centimeters |
height_cm | number | no | Height in centimeters |
is_fragile | boolean | no | Triggers fragile surcharge — default false |
declared_value | number | no | Insured value in TZS — default null |
When dimensions are provided, the engine computes volumetric weight = (L × W × H) / 5000 per item. The chargeable weight is max(actual_weight, volumetric_weight) — that's what regional transit charges against.
For same-city deliveries, weight only affects vehicle selection.
Response
{
"shipment_type": "same_city",
"vehicle_id": 1,
"vehicle_name": "Motorcycle",
"first_mile_fee": 3727,
"transit_fee": 0,
"last_mile_fee": 0,
"handling_fee": 0,
"service_fee": 373,
"fragile_surcharge": 0,
"total_amount": 4100,
"distance_km": 5.2,
"estimated_delivery_hours": 1,
"origin_hub": null,
"destination_hub": null
}
| Field | Type | Description |
|---|---|---|
shipment_type | string | "same_city" or "regional" |
vehicle_id | integer | The vehicle the engine selected |
vehicle_name | string | Display name (e.g. "Motorcycle", "Van") |
first_mile_fee | integer | Pickup → origin hub (regional) or full leg (same-city), TZS |
transit_fee | integer | Hub-to-hub transit, TZS — 0 for same-city |
last_mile_fee | integer | Destination hub → receiver, TZS — 0 for same-city |
handling_fee | integer | Hub handling, TZS — 0 for same-city |
service_fee | integer | 10% platform fee, TZS |
fragile_surcharge | integer | 15% surcharge if any item is fragile, TZS |
total_amount | integer | Sum — what your wallet will be debited |
distance_km | number | Total road distance (Google Distance Matrix, Haversine fallback) |
estimated_delivery_hours | integer | End-to-end estimate |
origin_hub | string | null | Hub name (regional only) |
destination_hub | string | null | Hub name (regional only) |
total_amount is the only number to surface to your end customer. The fee breakdown is for your accounting records.
Same-city vs regional pricing
The engine detects the type from distance:
Same-city (under ~50 km)
first_mile_fee = max(base_fee + distance × price_per_km, minimum_fee)
service_fee = 10% of first_mile_fee
total_amount = first_mile_fee + service_fee + fragile_surcharge
Vehicle base fees and per-km rates are in Vehicle Types.
Regional (50+ km)
first_mile_fee = pickup rider fee (sender → origin hub)
transit_fee = base_transit_rate + (chargeable_weight × per_kg_rate)
last_mile_fee = delivery rider fee (destination hub → receiver)
handling_fee = 2,000 TZS flat
fragile_surcharge = 15% of (first_mile + transit + last_mile + handling) if any item fragile
service_fee = 10% of all fees above
total_amount = sum of all fees
Errors
| Status | Detail |
|---|---|
400 | "Either coordinates or an address must be provided for pickup." |
400 | "Could not geocode dropoff address: '…'." — bad address string |
400 | "No transit route between origin and destination hubs." — regional path not yet served |
401 | Missing or invalid API key |