Developers · API v1
Tumira deliveries from your online store
When a customer checks out on your site, book a verified Tumira rider automatically. Your customer gets the rider's details, a live tracking link and their delivery code on WhatsApp, and your system hears about every step.
Get started
- Sign your shop up in the Tumira app (get it here) and set your shop's pin.
- Ask us for API keys on WhatsApp: a test key to build with and a live key when you're ready.
- Build against the test key. Test deliveries never reach riders: they get a "Test Rider" and move through every status in about two minutes.
- Swap in the live key. Deliveries are then booked and paid exactly as in the app.
Base URL: https://tumira.co.zw/api/v1. Requests and responses are JSON. Money is in US cents. Times are ISO 8601 (UTC).
curl https://tumira.co.zw/api/v1/me \
-H "Authorization: Bearer tk_test_…"
Keys
Send your key in the Authorization header: Bearer tk_live_… or Bearer tk_test_…. Keep keys on your server;
never put them in a web page or app. If a key leaks, tell us and we'll revoke it at once.
Live keys act as your shop: the same fare limits, cash-on-delivery rules and daily limits for new shops apply as in the app.
Quotes
POST /quotes: what a trip costs, for showing a delivery price at checkout.
curl https://tumira.co.zw/api/v1/quotes \
-H "Authorization: Bearer tk_test_…" -H "Content-Type: application/json" \
-d '{ "dropoff": { "address": "12 Lomagundi Rd, Avondale", "lat": -17.7931, "lng": 31.0361 }, "size": "small" }'
{
"object": "quote", "mode": "test", "currency": "USD", "size": "small",
"distance_km": 4.4, "suggested_cents": 300, "min_cents": 240, "max_cents": 900,
"dropoff": { "lat": -17.7931, "lng": 31.0361, "pin": "confirmed" }
}
Send dropoff.lat and dropoff.lng when you have them (from a map pin at checkout). With an address only, Tumira places it
itself and marks the pin "estimated"; the customer is then asked on WhatsApp to share their location so the rider finds them.
size is "small" (default) or "large".
Book a delivery
POST /deliveries. Send an Idempotency-Key header (for example your order number): retrying with the same key never books twice. Without it, a retry after a timeout
can book a second rider.
curl https://tumira.co.zw/api/v1/deliveries \
-H "Authorization: Bearer tk_test_…" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001" \
-d '{
"reference": "WC-1001",
"customer": { "name": "Rudo Moyo", "phone": "0771234567" },
"dropoff": { "address": "12 Lomagundi Rd, Avondale", "notes": "Blue gate", "lat": -17.7931, "lng": 31.0361 },
"size": "small",
"fee_paid_by": "shop",
"cash_on_delivery": { "amount_cents": 3500, "pay_to": "rider_cash" }
}'
customer.name, customer.phone | Required. The phone is the customer's WhatsApp number (07… or 2637…). |
dropoff.address, dropoff.lat, dropoff.lng | Address and/or pin; see Quotes. notes: gate, landmark. |
reference | Your own order number; you can look deliveries up by it. |
size | "small" (default) or "large". |
fare_cents | Optional. Offer more (or less) than the suggested fare, within the quote's min_cents–max_cents. |
fee_paid_by | "shop" (default; you pay the rider at pickup) or "customer" (they pay the rider at the door). |
cash_on_delivery | Optional. amount_cents to collect; pay_to: "shop_ecocash" (customer pays your EcoCash) or "rider_cash" (rider brings the cash back the same day). |
scheduled_for | Optional. Book for later, up to 7 days ahead. |
note | Optional note for the rider. |
Returns 201 with the delivery. Give the rider the pickup_code when they collect (print it on the parcel label if you like).
Look up deliveries
GET /deliveries/{id}: {id} is Tumira's id, the TM- code, or your reference.
GET /deliveries?status=searching&limit=20&before=2026-10-20T10:00:00Z: newest first, up to 100.
Cancel
POST /deliveries/{id}/cancel: before the rider picks the parcel up. Cancelling twice is fine (you get the cancelled delivery back).
After pickup you get 409 cannot_cancel: use "Cancel and bring it back" in the app.
The delivery
{
"id": "6f1c…", "object": "delivery", "mode": "live", "code": "TM-1042",
"status": "picked_up", "reference": "WC-1001",
"created_at": "2026-10-20T09:12:03Z", "scheduled_for": null,
"customer": { "name": "Rudo Moyo", "phone": "263771234567" },
"dropoff": { "address": "12 Lomagundi Rd, Avondale", "notes": "Blue gate", "lat": -17.7931, "lng": 31.0361, "pin": "confirmed" },
"size": "small",
"fare": { "amount_cents": 300, "suggested_cents": 300, "currency": "USD", "paid_by": "shop" },
"cash_on_delivery": { "amount_cents": 3500, "pay_to": "rider_cash", "collected": false },
"pickup_code": "4821",
"rider": { "name": "Tendai M.", "phone": "263771112222", "bike": "Honda ACE 125", "plate": "AFB 2193" },
"tracking_url": "https://tumira.co.zw/t/…",
"timestamps": { "assigned_at": "…", "picked_up_at": "…" }
}
searching | Looking for a rider. After 3 hours with none, Tumira cancels it and tells you. |
assigned | A rider is on the way to collect. rider is filled in. |
picked_up | The rider has the parcel (checked with your pickup code). |
arrived | The rider is at the customer. |
delivered | Handed over with the customer's delivery code. |
returning, returned | The customer wasn't reached; the parcel is coming back to you. |
cancelled | Cancelled by you, or no rider was found. |
Webhooks
Give us an https URL (one for test, one for live) and we POST every status change to it as an event. Answer with any 2xx within 10
seconds. Failed attempts are retried after 1 minute, 5 minutes, 30 minutes, 2, 6 and 12 hours. Events can arrive more than once (use the
event id to ignore repeats), and after a retry an older event can arrive after a newer one: compare
data.timestamps, or fetch the delivery, before going backwards.
{ "id": "evt_381", "object": "event", "type": "delivery.picked_up", "mode": "live",
"created_at": "2026-10-20T09:31:44Z", "data": { …the delivery… } }
Check the Tumira-Signature header, t=<unix seconds>,v1=<hex>: v1 is HMAC-SHA256 of
t + "." + the raw body with your signing secret. Reject it if it doesn't match or t is more than 5 minutes old.
// Node.js
const crypto = require('crypto');
function fromTumira(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const want = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return Math.abs(Date.now() / 1000 - Number(t)) < 300 &&
crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(want));
}
// PHP (WooCommerce)
function from_tumira($raw_body, $header, $secret) {
parse_str(str_replace(',', '&', $header), $p);
$want = hash_hmac('sha256', $p['t'] . '.' . $raw_body, $secret);
return abs(time() - (int) $p['t']) < 300 && hash_equals($want, $p['v1']);
}
Errors and limits
{ "error": { "code": "rejected", "message": "Offer a fare from $2.40 to $9.00 for this trip", "request_id": "req_9c1e…" } }
400 | Something in the request is missing or malformed (invalid_customer, invalid_dropoff, …). |
401 / 403 | Missing, wrong or revoked key; or the shop isn't active. |
404 | No such delivery for this key's mode. |
409 | Not possible in the delivery's current state (cannot_cancel), or the key needs replacing (shop_not_ready). |
422 | Tumira's rules said no (rejected, outside_area, address_not_found). The message says why, in words you can show your team. |
429 | More than 120 requests a minute for one key. |
500 | Our side. Retry with the same Idempotency-Key; quote the request_id if you contact us. |
Questions? WhatsApp +263 71 488 8633 or [email protected].
Test mode notes
Test deliveries use a fixed pickup code (1234), no real tracking page, and are deleted after 30 days. No WhatsApp messages go out.