Skip to main content

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.

FieldTypeRequiredDescription
pickup_latitudenumberconditionalPickup latitude (-90 to 90)
pickup_longitudenumberconditionalPickup longitude (-180 to 180)
pickup_addressstringconditionalPickup address (used if coords omitted)
dropoff_latitudenumberconditionalDropoff latitude
dropoff_longitudenumberconditionalDropoff longitude
dropoff_addressstringconditionalDropoff address (used if coords omitted)
itemsarrayyes1–50 items, see below
vehicle_type_idintegernoForce a specific vehicle (otherwise auto-selected — see Vehicle Types)

Item fields​

The engine is dimensional. Every item must carry weight_kg + length_cm + width_cm + height_cm — that's how vehicle selection and chargeable weight are computed.

FieldTypeRequiredDescription
descriptionstringyesWhat the item is (2–500 chars)
weight_kgnumberyesActual weight in kilograms (0 < w ≤ 500)
length_cmnumberyesLength in centimeters (0 < l ≤ 500)
width_cmnumberyesWidth in centimeters (0 < w ≤ 500)
height_cmnumberyesHeight in centimeters (0 < h ≤ 500)
quantityintegeryesNumber of units (1–100)
is_fragilebooleannoTriggers fragile surcharge — default false
declared_valuenumbernoInsured value in TZS — default null
"items": [
{
"description": "Food order — 2 containers",
"weight_kg": 2.5,
"length_cm": 25,
"width_cm": 25,
"height_cm": 15,
"quantity": 1
}
]
Chargeable weight

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 + dimensions only affect vehicle selection — but the engine still needs them on every item so the same payload works regardless of destination.

Package size tiers — client-side UX convenience​

If your end users don't think in millimetres, render a tier picker in your own UI and expand the picked tier into raw weight_kg + L + W + H before posting. The platform publishes a curated preset list you can copy:

curl https://api.dodo.co.tz/api/v1/package-size-tiers
[
{ "code": "envelope", "label": "Envelope", "length_cm": 30, "width_cm": 22, "height_cm": 2, "weight_kg": 0.5, "sort_order": 1 },
{ "code": "small", "label": "Small", "length_cm": 25, "width_cm": 20, "height_cm": 15, "weight_kg": 3, "sort_order": 2 },
{ "code": "medium", "label": "Medium", "length_cm": 40, "width_cm": 30, "height_cm": 25, "weight_kg": 10, "sort_order": 3 },
...
]

The list is admin-curated and can change. Refresh it on app start (or every few hours) and use the values to populate your item form before submit. The server never sees a tier code — it sees the dimensions the tier expands to.

See Package Size Tiers for the full preset catalog and typical-contents guidance.

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_minutes": 75,
"estimated_delivery_hours": 1,
"origin_hub": null,
"destination_hub": null
}
FieldTypeDescription
shipment_typestring"same_city" or "regional"
vehicle_idintegerThe vehicle the engine selected
vehicle_namestringDisplay name (e.g. "Motorcycle", "Van")
first_mile_feeintegerPickup → origin hub (regional) or full leg (same-city), TZS
transit_feeintegerHub-to-hub transit, TZS — 0 for same-city
last_mile_feeintegerDestination hub → receiver, TZS — 0 for same-city
handling_feeintegerHub handling, TZS — 0 for same-city
service_feeintegerPlatform fee, TZS (currently 10%)
fragile_surchargeintegerFragile-item surcharge, TZS (currently 15%; zero unless an item has is_fragile: true)
total_amountintegerSum — what your wallet will be debited
distance_kmnumberTotal road distance
estimated_delivery_minutesintegerEnd-to-end estimate, in minutes — prefer this over estimated_delivery_hours for any same-city UI
estimated_delivery_hoursintegerEnd-to-end estimate, integer hours rounded from estimated_delivery_minutes. Kept for back-compat.
origin_hubstring | nullHub name (regional only)
destination_hubstring | nullHub name (regional only)
tip

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. The same-city / regional boundary, the service-fee percent, the fragile-surcharge percent, and the flat handling fee are all admin-configurable — the numbers below are current defaults, not contract.

Same-city (under ~50 km)​

first_mile_fee  = max(base_fee + distance × price_per_km, minimum_fee)
service_fee = service_fee_percent × first_mile_fee (currently 10%)
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 = transit_base_rate + (chargeable_weight × transit_per_kg_rate)
last_mile_fee = delivery rider fee (destination hub → receiver)
handling_fee = flat hub handling fee (currently 2,000 TZS)
fragile_surcharge = fragile_percent × (first_mile + transit + last_mile + handling)
(currently 15%, only if any item fragile)
service_fee = service_fee_percent × (subtotal + fragile_surcharge)
(currently 10%)
total_amount = sum of all fees

Always trust total_amount from the response rather than re-computing on your side — admins can adjust the knobs without warning to react to fuel prices or partner rates.

Errors​

StatusDetail
400"Either coordinates or an address must be provided for pickup."
400"Could not geocode dropoff address: '…'." — bad address string
400"No vehicle can carry this load — payload Xkg, volumetric Ykg, longest side Zcm…" — no active vehicle can take all items (mass, volume, or length cap exceeded). Split the shipment, drop fragile items, or pick a smaller package.
400"No transit route between origin and destination hubs." — regional path not yet served
401Missing or invalid API key
422Item is missing one of weight_kg, length_cm, width_cm, or height_cm — all four are required on every item.