Skip to main content

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

CodeMeaningTypical cause
400Bad requestBusiness rule violation (insufficient funds, invalid state, ungeocodable address)
401UnauthorizedMissing, wrong, or revoked X-API-Key
403ForbiddenMerchant account not approved or suspended
404Not foundShipment doesn't exist or doesn't belong to your account
422Validation errorField-level issues — check detail array
429Rate limitedToo many requests — see Rate limits
500Server errorTransient — retry with backoff
502Upstream errorPayment gateway or maps service unavailable — retry

Common errors and how to handle them

401 Unauthorized

{ "detail": "Invalid or expired API key" }
  • Check X-API-Key header is set and the value is intact (no extra whitespace, no Bearer prefix)
  • 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:

BucketLimit
Rate quotes120 requests / minute
Create / cancel60 requests / minute
List / get600 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 classRetry?How
4xx (except 429)NoThese won't succeed without a code change
429YesHonor Retry-After. Use exponential backoff if absent.
500, 502, 503, 504YesExponential backoff, capped at 5 attempts
Network timeoutsYesSame as 5xx — but check via GET /merchant/shipments?merchant_reference=... first to avoid duplicate creation
Idempotency on create

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.