Skip to content
DocsProductSign inCreate an account

Delivery API

Tracking

Every delivery carries an unguessable token. Hand it to your customer as a link and they watch the rider approach — no account, no login, no app. Or consume the same feed yourself and draw the pin in your own interface.

The link

Creating a delivery returns a trackingToken. Append it to the tracking path and you have a page you can text, email or show as a QR code.

Customer-facing link
https://dispatch.gumza.ke/track/<trackingToken>

If your customers ordered through a Gumza WhatsApp storefront, they already have a link — it is sent into the chat thread automatically as the order moves, alongside each status update, and it points at https://gumza.ke/track/<token> with the order's own token.

Reading it yourself

Both endpoints are public — the token in the URL is the authorisation, so no API key is sent and none should be.

  • GET/api/v1/track/orders/:tokenCurrent status, both addresses, last known rider location
  • GET/api/v1/track/orders/:token/streamServer-sent events: rider position as it changes
GET /api/v1/track/orders/:token
{
  "success": true,
  "data": {
    "status": "IN_TRANSIT",
    "pickup":  { "address": "Shop 4, Westlands, Nairobi", "lat": -1.2672, "lng": 36.8065 },
    "dropoff": { "address": "Kileleshwa, Nairobi", "lat": -1.2833, "lng": 36.7833 },
    "updatedAt": "2026-10-12T09:11:40.220Z",
    "rider": { "location": { "lat": -1.2744, "lng": 36.7961, "bearing": 112,
                             "updatedAt": "2026-10-12T09:11:38.004Z" } }
  }
}

rider is null until someone is assigned, and rider.location is null until that rider's device has reported a position. Handle both — a map that throws before the first ping is the most common bug here.

The live stream

The stream endpoint is Server-Sent Events, not a WebSocket, so a plain EventSource works with no library and reconnects on its own.

Browser
const source = new EventSource(
  `https://api.dispatch.gumza.ke/api/v1/track/orders/${token}/stream`
);

source.onmessage = (event) => {
  const { status, location } = JSON.parse(event.data);
  marker.setLatLng([location.lat, location.lng]);
  label.textContent = status;
};

// EventSource retries by itself; close it when the view goes away.
source.onerror = () => { /* surface a "reconnecting" hint, do not close */ };
window.addEventListener('beforeunload', () => source.close());
  • Each frame is { status, location }, where location is { lat, lng, bearing?, updatedAt } — the same shape as the read endpoint.
  • A message is pushed only when the position actually changes — no duplicate frames.
  • A comment line is sent every 15 seconds to keep the connection alive through proxies and mobile networks. EventSource ignores it; you will not see it in onmessage.
  • Before a rider is assigned there is nothing to stream. The connection stays open and quiet, so render the status from the read endpoint rather than waiting for a frame.

Position in your own backend

If you want rider position server-side rather than in a browser, subscribe to the dispatch.location_updated webhook — see Webhooks. It is throttled to at most one event every 20 seconds per rider, which is plenty for a map and gentle on your endpoint.

One wrinkle worth knowing before you write the mapping code: that webhook names its coordinates latitude and longitude with a timestamp, while the tracking read and stream above use lat, lng and updatedAt. Same numbers, different keys.