Developers
Tracking a delivery
Two ways to show a customer where their parcel is: the status it has reached, or the rider moving on a map. Pick one per product — the first is a webhook you already receive, the second is one open endpoint you poll.
Two levels
Status only
No map, no coordinates, no polling loop. You already have this: every status change arrives at your webhook, and GET /partner/deliveries/{id} answers the same question on demand. Render the five states as a progress line and you have told the customer everything that matters about where their parcel is — accepted, picked up, in transit, delivered. Most integrations should stop here. It costs nothing to run and there is nothing to keep in sync.
Best for: Order pages, confirmation emails, anything that is read once or twice.
The rider's live position
Coordinates you can plot on your own map. GET /track/{delivery_id}/public returns the route, the per-drop ETAs and, while the trip is in flight, the rider's current latitude and longitude. It needs no API key — the delivery id is the credential — so you can call it straight from the customer's browser and draw the dot with whatever map library you already use.
Best for: A live tracking page a customer sits and watches while they wait.
Status only
Nothing new to call. The webhook you already receive carries the status, and the delivery endpoint answers on demand if you cannot be called back. Map the five forward states onto a progress line and you are done — that is what most customers actually read.
# The webhook already told you this:
# { "event": "delivery.in_transit", "status": "in_transit", … }
# Ask on demand when you cannot be called back:
curl https://api.okadadrop.com/api/v1/partner/deliveries/8f3b2c1e-… \
-H "Authorization: Bearer $SOMA_API_KEY"The rider’s live position
One open endpoint carries everything a map needs: both ends of the route, each drop on a multi-drop trip, the arrival estimate, and the rider’s current coordinates while they are riding.
# No key. The delivery id is the credential, so this is safe
# to call from your customer's browser.
curl https://api.okadadrop.com/api/v1/track/8f3b2c1e-…/public{
"reference": "SOMA-8F3B2C1E",
"status": "in_transit",
"package_size": "small",
"pickup_address": "Osu, Accra",
"pickup_lat": 5.5560, "pickup_lng": -0.1820,
"dropoff_address": "East Legon, Accra",
"dropoff_lat": 5.6360, "dropoff_lng": -0.1530,
"recipient_name": "Ama M.",
"eta_min": 12,
"rider": {
"name": "Kwame A.",
"vehicle": "Honda CG125",
"rating": 4.8,
"lat": 5.5900,
"lng": -0.1700
},
"stops": [],
"events": [
{ "status": "pending", "lat": null, "lng": null, "created_at": "…" },
{ "status": "accepted", "lat": 5.5561, "lng": -0.1822, "created_at": "…" },
{ "status": "picked_up", "lat": 5.5560, "lng": -0.1820, "created_at": "…" }
]
}What comes back
| Field | Type | Notes |
|---|---|---|
| reference | Typestring | NotesThe short human reference — SOMA-8F3B2C1E. What a customer quotes at you on the phone. |
| status | Typestring | NotesThe same seven values the webhook sends. |
| pickup_lat / pickup_lng | Typenumber | NotesWhere the parcel was collected. Always present. |
| dropoff_lat / dropoff_lng | Typenumber | NotesThe final destination. Always present. |
| eta_min | Typenumber | null | NotesMinutes to the final drop, traffic-aware. Null when the trip is not live. |
| rider.name / vehicle / rating | Typestring | null | NotesEnough to recognise the person at the door: a first name and an initial, the bike, a rating. |
| rider.lat / rider.lng | Typenumber | null | NotesThe live position. Null until a rider is on the job, and again after the drop — see below. |
| stops[] | Typearray | NotesEach earlier drop on a multi-drop trip, with its own coordinates, status and cumulative eta_min. |
| events[] | Typearray | NotesThe timeline: every status change with a timestamp, and the coordinate it happened at. |
Polling it
const TERMINAL = ["delivered", "cancelled", "expired"];
// Poll while it moves, stop when it lands. An interval left running on a
// finished delivery is the usual way integrations burn their rate limit.
export function track(deliveryId, onUpdate) {
let timer;
const tick = async () => {
const res = await fetch(
`https://api.okadadrop.com/api/v1/track/${deliveryId}/public`,
);
if (!res.ok) return; // 404 = wrong id; anything else, try again next tick
const data = await res.json();
onUpdate(data);
if (!TERMINAL.includes(data.status)) timer = setTimeout(tick, 6000);
};
tick();
return () => clearTimeout(timer);
}Putting it on a map
The payload is coordinates and nothing else, so it fits whatever map you already run. Two rules carry most of the correctness: draw the route from the fields that are always present, and treat the rider’s dot as something that may be missing at any moment.
// Any map library works — this is Google Maps, but the payload is
// only coordinates, so Leaflet, Mapbox or MapLibre are the same few lines.
const map = new google.maps.Map(el, { center: pickup, zoom: 13 });
const rider = new google.maps.Marker({ map, icon: RIDER_PIN });
track(deliveryId, (data) => {
// The route is always there.
drawRoute(map, [
{ lat: data.pickup_lat, lng: data.pickup_lng },
...data.stops.map((s) => ({ lat: s.dropoff_lat, lng: s.dropoff_lng })),
{ lat: data.dropoff_lat, lng: data.dropoff_lng },
]);
// The rider is not. Null before one accepts, after the drop, and any
// time their app stops reporting — so hide the dot, never throw.
const live = data.rider && data.rider.lat != null;
rider.setVisible(Boolean(live));
if (live) rider.setPosition({ lat: data.rider.lat, lng: data.rider.lng });
setEta(data.eta_min); // null when the trip is not live
});Getting it right
The rider's position is optional, always
rider.lat and rider.lng are null before a rider accepts, after the parcel is delivered, and any time that rider's app stops reporting — a dead battery, a tunnel, a phone in a pocket with no signal. We would rather show you nothing than a dot parked where the rider was ten minutes ago. Draw the route first and treat the dot as something that may or may not be there; a map that throws when the field is null will break on a perfectly normal delivery.
Poll every 5–10 seconds, and stop
There is no push channel on this endpoint. Poll while the delivery is live and stop the moment it reads delivered, cancelled or expired — a tab left open on a finished delivery is the most common way to burn a rate limit. Faster than five seconds buys you nothing either: the ETA is recomputed at most every 20 seconds behind the endpoint.
The id is the credential
No key, no signature — anyone holding the delivery id can read this. That is what makes it safe to put in your customer's browser, and it is why the response is redacted: no phone numbers, no fare, no handover code, and names cut to a first name and an initial. Treat a delivery id like an unlisted link. If you need the full record, call /partner/deliveries/{id} from your server with your key.
Call it from the browser if you like
The endpoint answers cross-origin requests from any site, so a tracking page can fetch it directly with no proxy of your own in the middle. Doing it server-side works exactly the same way — it is the same URL either way.
Or send them to ours
Every delivery has a page on this site already, with the same map: link your customer to /track?id={delivery_id} and there is nothing to build, host or keep working. Worth taking seriously before you write a tracking page of your own — it is the only option here with no code in it.
https://okadadrop.com/track?id=8f3b2c1e-…