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.