Manage Shipments
Once a shipment is created, use these endpoints to list, inspect, and cancel.
List shipments
GET /merchant/shipments
Returns your shipments, most recent first.
curl "https://api.dodo.co.tz/api/v1/merchant/shipments?status=delivered&page=1&per_page=20" \
-H "X-API-Key: $DODO_API_KEY"
Query parameters:
| Param | Type | Description |
|---|---|---|
status | string | Filter by shipment status — pending, confirmed, pickup_dispatched, picked_up, in_transit, out_for_delivery, delivered, cancelled, failed |
type | string | same_city or regional |
search | string | Match against shipment_number, tracking_code, or merchant_reference |
page | integer | Page number (default: 1) |
per_page | integer | Page size (default: 20, max: 100) |
Response (200 OK):
[
{
"id": 12345,
"shipment_number": "SHP-20260515-A1B2C3",
"tracking_code": "DODO7K9M2P4QR8",
"merchant_reference": "your-internal-order-id-123",
"type": "same_city",
"status": "delivered",
"sender_name": "Malaika Restaurant",
"receiver_name": "Jane Doe",
"pickup_address": "Samora Ave, Dar es Salaam",
"dropoff_address": "Apartment 4B, Msasani, Dar es Salaam",
"total_amount": 4100,
"payment_status": "paid",
"estimated_delivery_at": "2026-05-15T11:30:00Z",
"created_at": "2026-05-15T10:30:00Z"
}
]
The list endpoint returns the trimmed list-view object — call Get details for items, legs, and the event log.
Get shipment details
GET /merchant/shipments/{id}
Full shipment record including items, journey legs, and the complete event log.
curl https://api.dodo.co.tz/api/v1/merchant/shipments/12345 \
-H "X-API-Key: $DODO_API_KEY"
The response shape matches the POST /merchant/shipments response, but with up-to-date status and a populated events array.
Event log
events is the timeline of what happened to this shipment. Use it to display delivery progress in your UI when you can't easily wire up webhooks.
"events": [
{
"id": 102,
"status": "delivered",
"leg_type": "first_mile",
"description": "Package delivered to Jane Doe",
"location_address": null,
"actor_type": "rider",
"occurred_at": "2026-05-15T11:28:14Z"
},
{
"id": 101,
"status": "out_for_delivery",
"leg_type": "first_mile",
"description": "Rider James M. heading to receiver",
"actor_type": "rider",
"occurred_at": "2026-05-15T11:05:42Z"
},
{
"id": 100,
"status": "picked_up",
"leg_type": "first_mile",
"description": "Package collected from Malaika Restaurant",
"actor_type": "rider",
"occurred_at": "2026-05-15T10:48:09Z"
}
]
Events are ordered newest first.
Cancel a shipment
POST /merchant/shipments/{id}/cancel
Cancellation is state-graded — what you pay depends on what the rider has already done. The endpoint is a two-step preview / commit flow so you can surface the fee to your user before charging the wallet.
Fee table
| Window | Trigger | Fee |
|---|---|---|
grace | < 60 s since creation, no rider has accepted yet | 0 TZS — full refund |
assigned | Rider has accepted but isn't moving yet | 30% of first_mile_fee |
en_route | Rider is moving toward pickup (or has arrived) | 100% of first_mile_fee |
forbidden | Rider has the package (picked_up and beyond) | Cannot cancel — create a return-to-sender shipment instead |
The fee is retained; the remainder is credited back to your wallet — regardless of whether the original payment came from wallet or gateway. Gateway-paid shipments do not trigger a Selcom reverse charge; the refund lands as wallet balance you can spend on your next shipment.
Step 1 — Preview the fee
Call without confirm=true to get a quote. No state change.
curl -X POST https://api.dodo.co.tz/api/v1/merchant/shipments/12345/cancel \
-H "X-API-Key: $DODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Customer changed their mind" }'
Response (200):
{
"status": "cancellation_pending",
"shipment_id": 12345,
"shipment_number": "SHP-20260515-A1B2C3",
"window": "assigned",
"fee": 800,
"refund": 3300,
"currency": "TZS",
"reason_for_fee": "Rider has accepted but not yet started moving (30% of first-mile fee retained)."
}
Surface reason_for_fee to your end user — "Cancel this order? Your wallet will be charged 800 TZS because a rider has been dispatched."
Step 2 — Commit
Re-send the same request with ?confirm=true to commit:
curl -X POST "https://api.dodo.co.tz/api/v1/merchant/shipments/12345/cancel?confirm=true" \
-H "X-API-Key: $DODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Customer changed their mind" }'
Response (200):
{
"status": "cancelled",
"shipment_id": 12345,
"shipment_number": "SHP-20260515-A1B2C3",
"window": "assigned",
"fee_debited": 800,
"refund_credited": 3300,
"currency": "TZS",
"shipment": { /* full shipment record */ }
}
The fee is recomputed at commit time. If the shipment has advanced a window between your preview and your commit (e.g. the rider started moving), the new (higher) fee applies — re-preview to see what your user is now agreeing to.
Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | yes | Why you're cancelling (5–500 chars) — appears in admin reports |
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
confirm | boolean | false | Set to true to commit; otherwise returns a fee preview |
Once a rider has the package (picked_up and beyond), cancellation is forbidden. The API returns:
{ "detail": "Rider already has the package. Create a return-to-sender shipment instead of cancelling." }
What happens on commit
- Cancellation fee retained as platform revenue (if
fee > 0) - Remainder credited back to wallet as a
REFUNDtransaction — works the same for wallet-paid and gateway-paid shipments; gateway-paid funds become wallet balance rather than going back through Selcom - Shipment status flips to
cancelled - Webhook
shipment.cancelledfires with{ window, fee, refund, reason } - If a rider was dispatched, they're notified the job is cancelled
Related
- Webhooks — get status updates pushed to your server instead of polling
- Shipment Lifecycle — full state machine including which transitions can cancel
- Errors — error codes and retry strategy