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 — 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.
| 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) |
length_cm | number | yes | Length in centimeters (0 < l ≤ 500) |
width_cm | number | yes | Width in centimeters (0 < w ≤ 500) |
height_cm | number | yes | Height in centimeters (0 < h ≤ 500) |
quantity | integer | yes | Number of units (1–100) |
is_fragile | boolean | no | Triggers fragile surcharge — default false |
declared_value | number | no | Insured 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
}
]
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
}
| 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 | Platform fee, TZS (currently 10%) |
fragile_surcharge | integer | Fragile-item surcharge, TZS (currently 15%; zero unless an item has is_fragile: true) |
total_amount | integer | Sum — what your wallet will be debited |
distance_km | number | Total road distance |
estimated_delivery_minutes | integer | End-to-end estimate, in minutes — prefer this over estimated_delivery_hours for any same-city UI |
estimated_delivery_hours | integer | End-to-end estimate, integer hours rounded from estimated_delivery_minutes. Kept for back-compat. |
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. 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
| Status | Detail |
|---|---|
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 |
401 | Missing or invalid API key |
422 | Item is missing one of weight_kg, length_cm, width_cm, or height_cm — all four are required on every item. |