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" \
-H "Idempotency-Key: 9c1e7e92-2a4d-4b9f-9b1a-2c3e4f5a6b7c" \
-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,
"length_cm": 25,
"width_cm": 25,
"height_cm": 15
}
],
"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. Each item must carry description, quantity, weight_kg, and the three dimensions (length_cm, width_cm, height_cm). is_fragile and declared_value are optional. The engine is dimensional — vehicle selection and chargeable-weight pricing come from these values.
"items": [
{
"description": "Glass decorations",
"weight_kg": 1.2,
"length_cm": 25,
"width_cm": 25,
"height_cm": 30,
"quantity": 1,
"is_fragile": true,
"declared_value": 50000
}
]
If your end users don't think in millimetres, build a tier picker in your own UI and expand the picked tier into these fields before submit — see Package Size Tiers for the preset catalog the platform publishes for that purpose.
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. Overrides still go through the three-axis capacity check; an undersized override returns 400. See Vehicle Types. |
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.
Safe to retry — Idempotency-Key header
Network blips between your server and ours can leave you unsure whether a create succeeded. To make retries safe, send a unique Idempotency-Key header with each create attempt:
curl -X POST https://api.dodo.co.tz/api/v1/merchant/shipments \
-H "X-API-Key: $DODO_API_KEY" \
-H "Idempotency-Key: 7a4c8e2b-1f3d-4b6e-9a8d-2c5f0e7b1d4a" \
-H "Content-Type: application/json" \
-d '{ ... }'
If we receive a second request with the same key within 24 hours, we return the first response verbatim instead of creating a duplicate shipment (and double-debiting your wallet). Use a fresh UUID per intended create; reuse the same key when retrying the same create.
Header is optional but strongly recommended for any production path that might retry. Without it, retries can produce duplicate shipments. The same header is honored on /cancel and /payment/initiate.
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 |
400 | "No vehicle can carry this load — payload …kg, volumetric …kg, longest side …cm…" | The combined items exceed every active vehicle's mass / volume / longest-side cap. Split the shipment, or revisit dimensions / fragility flags. |
400 | "Vehicle '…' cannot carry this load — …" | Your vehicle_type_id override is too small. Drop the override or pick a larger vehicle. |
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 (e.g. item missing one of weight_kg, length_cm, width_cm, height_cm, or invalid phone format) |
Related
- Manage Shipments — list, get, cancel
- Webhooks — real-time status events
- Shipment Lifecycle — the full status state machine