Errors
All errors return a JSON body with HTTP status codes. Most errors include a single detail string; validation errors include field-level details.
Standard error shape
{
"detail": "Insufficient wallet balance. Required: 4100 TZS, Available: 2500 TZS."
}
Validation error shape (422)
{
"detail": [
{
"loc": ["body", "receiver_phone"],
"msg": "Phone number must start with country code (e.g. +255...)",
"type": "value_error"
},
{
"loc": ["body", "items", 0, "weight_kg"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt"
}
]
}
loc is the path to the offending field — useful for surfacing inline form errors.
HTTP status codes
| Code | Meaning | Typical cause |
|---|---|---|
400 | Bad request | Business rule violation (insufficient funds, invalid state, ungeocodable address) |
401 | Unauthorized | Missing, wrong, or revoked X-API-Key |
403 | Forbidden | Merchant account not approved or suspended |
404 | Not found | Shipment doesn't exist or doesn't belong to your account |
422 | Validation error | Field-level issues — check detail array |
429 | Rate limited | Too many requests — see Rate limits |
500 | Server error | Transient — retry with backoff |
502 | Upstream error | Payment gateway or maps service unavailable — retry |
Common errors and how to handle them
401 Unauthorized
{ "detail": "Invalid or expired API key" }
- Check
X-API-Keyheader is set and the value is intact (no extra whitespace, noBearerprefix) - Confirm you're using a sandbox key against the sandbox URL, and live key against production
- If the key was rotated, get the new one from the dashboard
403 Forbidden
{ "detail": "Merchant account not approved" }
- Your account is awaiting admin review. You'll get an email once approved.
- If approved and still seeing this, your account may be suspended — contact support.
400 Insufficient balance
{ "detail": "Insufficient wallet balance. Required: 4100 TZS, Available: 2500 TZS." }
- Top up your wallet in the dashboard
- Set up low-balance alerts to avoid this in production
400 Cannot cancel
{ "detail": "Shipment in status 'picked_up' cannot be cancelled." }
- Cancellation is only allowed before pickup. See Lifecycle.
- For post-pickup issues, contact support
400 Ungeocodable address
{ "detail": "Could not geocode dropoff address: 'somewhere in Dar'." }
- The address string is too vague. Send GPS coordinates (
dropoff_latitude+dropoff_longitude) instead.
422 Validation
Field-level issue — read the loc path:
{ "loc": ["body", "items", 0, "weight_kg"], "msg": "ensure this value is greater than 0" }
Means items[0].weight_kg failed the > 0 constraint. Fix and retry.
Rate limits
The API enforces per-merchant rate limits:
| Bucket | Limit |
|---|---|
| Rate quotes | 120 requests / minute |
| Create / cancel | 60 requests / minute |
| List / get | 600 requests / minute |
On hitting a limit:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
{ "detail": "Rate limit exceeded. Retry after 30 seconds." }
Respect the Retry-After header. Bulk operations should be paced — don't fan out 100 POST /merchant/shipments calls in a tight loop.
Retry strategy
| Error class | Retry? | How |
|---|---|---|
4xx (except 429) | No | These won't succeed without a code change |
429 | Yes | Honor Retry-After. Use exponential backoff if absent. |
500, 502, 503, 504 | Yes | Exponential backoff, capped at 5 attempts |
| Network timeouts | Yes | Same as 5xx — but check via GET /merchant/shipments?merchant_reference=... first to avoid duplicate creation |
POST /merchant/shipments is not idempotent. If you retry a creation that actually succeeded, you'll create a duplicate (and double-debit your wallet).
When in doubt, check first:
curl "https://api.dodo.co.tz/api/v1/merchant/shipments?search=your-internal-order-id-123" \
-H "X-API-Key: $DODO_API_KEY"
If a shipment with your merchant_reference already exists, don't recreate it.
Logging
Log the full response body — not just the status code — when you handle an error. The detail field is the actionable signal. For 422 errors, log the full detail array so you can replay validation failures.