Create a Shipment
Create and immediately pay for a delivery in a single call. Dodo debits your wallet, dispatches a rider, and starts firing webhooks within seconds.
POST /merchant/shipments
curl -X POST https://api.dodo.co.tz/api/v1/merchant/shipments \
-H "X-API-Key: $DODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sender_name": "Malaika Restaurant",
"sender_phone": "+255712345678",
"pickup_address": "Samora Ave, Dar es Salaam",
"pickup_latitude": -6.7924,
"pickup_longitude": 39.2083,
"pickup_notes": "Ask for the manager at the counter",
"receiver_name": "Jane Doe",
"receiver_phone": "+255765432109",
"dropoff_address": "Apartment 4B, Msasani, Dar es Salaam",
"dropoff_latitude": -6.7600,
"dropoff_longitude": 39.2400,
"dropoff_notes": "Call on arrival — gate code 1234",
"items": [
{ "description": "Food order — 2 containers", "weight_kg": 2.5, "quantity": 1 }
],
"payment_source": "wallet",
"merchant_reference": "your-internal-order-id-123"
}'
Request
Sender (your pickup point)
| Field | Type | Required | Description |
|---|---|---|---|
sender_name | string | yes | Your business or contact name |
sender_phone | string | yes | E.164 format: +255712345678 |
sender_email | string | no | Email for delivery receipts |
Pickup location
| Field | Type | Required | Description |
|---|---|---|---|
pickup_address | string | yes | Human-readable pickup address (5–500 chars) |
pickup_latitude | number | no | Strongly recommended for accurate dispatch |
pickup_longitude | number | no | Required if pickup_latitude is sent |
pickup_notes | string | no | Free-text instructions for the rider (≤ 500 chars) |
Receiver (your end customer)
| Field | Type | Required | Description |
|---|---|---|---|
receiver_name | string | yes | Full name (2–255 chars) |
receiver_phone | string | yes | E.164 — receives the tracking-link SMS |
receiver_email | string | no | Email for delivery confirmation |
Dropoff location
| Field | Type | Required | Description |
|---|---|---|---|
dropoff_address | string | yes | Human-readable dropoff address |
dropoff_latitude | number | no | Strongly recommended |
dropoff_longitude | number | no | Required if dropoff_latitude is sent |
dropoff_notes | string | no | Free-text instructions for the rider |
Items
items is an array of 1–50 items. Same fields as the rate quote:
"items": [
{
"description": "Glass decorations",
"weight_kg": 1.2,
"quantity": 1,
"length_cm": 25,
"width_cm": 25,
"height_cm": 30,
"is_fragile": true,
"declared_value": 50000
}
]
Payment & metadata
| Field | Type | Required | Description |
|---|---|---|---|
payment_source | string | yes | Must be "wallet" for S2S integrations |
vehicle_type_id | integer | no | Force a vehicle — otherwise auto-selected by weight |
merchant_reference | string | no | Your internal order ID — stored on the shipment and echoed in every webhook |
payment_source: "wallet"If omitted, the field defaults to "gateway" — which means Dodo creates the shipment in pending status and waits for a Selcom payment URL flow. That path is for cases where your end customer pays Dodo directly. For server-to-server merchant integrations you almost always want wallet.
merchant_reference for correlationSet this to your own order ID. It comes back in every webhook payload so you can match Dodo events to your records without storing Dodo's internal id.
Response
{
"id": 12345,
"shipment_number": "SHP-20260515-A1B2C3",
"tracking_code": "DODO7K9M2P4QR8",
"merchant_reference": "your-internal-order-id-123",
"type": "same_city",
"status": "confirmed",
"vehicle_type_name": "Motorcycle",
"sender_name": "Malaika Restaurant",
"sender_phone": "+255712345678",
"pickup_address": "Samora Ave, Dar es Salaam",
"pickup_latitude": -6.7924,
"pickup_longitude": 39.2083,
"pickup_notes": "Ask for the manager at the counter",
"receiver_name": "Jane Doe",
"receiver_phone": "+255765432109",
"dropoff_address": "Apartment 4B, Msasani, Dar es Salaam",
"dropoff_latitude": -6.76,
"dropoff_longitude": 39.24,
"dropoff_notes": "Call on arrival — gate code 1234",
"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,
"payment_status": "paid",
"payment_confirmed_at": "2026-05-15T10:30:01Z",
"estimated_delivery_at": "2026-05-15T11:30:00Z",
"created_at": "2026-05-15T10:30:00Z",
"items": [...],
"legs": [...],
"events": [...]
}
| Field | Type | Description |
|---|---|---|
id | integer | Dodo's internal ID — store for webhook matching |
shipment_number | string | Human reference (SHP-YYYYMMDD-XXXXXX) |
tracking_code | string | Public tracking token (DODOXXXXXXXXXX) — share with your customer |
status | string | "confirmed" immediately after wallet debit. See Shipment Lifecycle |
type | string | "same_city" or "regional" |
total_amount | integer | Amount debited from your wallet (TZS) |
payment_status | string | "paid" for wallet payments |
payment_confirmed_at | string | ISO timestamp when payment cleared |
estimated_delivery_at | string | ETA — populated based on distance and vehicle |
legs | array | Journey legs (1 for same-city, 3 for regional). See Lifecycle |
events | array | Tracking event log — empty at creation, grows as the shipment moves |
What happens next
POST /merchant/shipments (status=pending, transient)
│
▼
Wallet debit succeeds → status=confirmed → webhook: shipment.created (and shipment.paid)
│
▼
Rider auto-dispatched → status=pickup_dispatched
│
▼
Rider collects package → status=picked_up → webhook: delivery.picked_up
│
▼
Rider heads to receiver → status=out_for_delivery
│
▼
Delivered → status=delivered → webhook: delivery.delivered
Set up Webhooks to receive each transition on your server.
Sharing tracking with your customer
The recipient automatically gets an SMS with the tracking URL when the shipment is confirmed:
Your delivery is on its way. Track here:
https://dododelivery.co.tz/track?code=DODO7K9M2P4QR8
If you want to surface the same tracking link in your own UI (order confirmation page, email, etc.), use the tracking_code from the response:
https://dododelivery.co.tz/track?code={tracking_code}
See Tracking for details.
Errors
| Status | Detail | Cause |
|---|---|---|
400 | "Insufficient wallet balance. Required: 4100 TZS, Available: 2500 TZS." | Top up your wallet |
400 | "Merchant wallet is not active." | Wallet was suspended — contact support |
400 | "Either coordinates or an address must be provided for pickup." | Send coords or an address for both endpoints |
400 | "Could not geocode dropoff address: '…'." | Geocoding failed — send GPS coordinates instead |
401 | "Invalid or expired API key" | Check your X-API-Key header |
403 | "Merchant account not approved" | Wait for admin approval |
422 | Validation error — see field-level details | Bad request body |
Related
- Manage Shipments — list, get, cancel
- Webhooks — real-time status events
- Shipment Lifecycle — the full status state machine