Delivery API
API reference
A small REST surface over HTTPS. One header to authenticate, one to make writes safe to retry, and a JSON envelope on every response. No SDK required — if your language can POST, you can integrate.
Base URL
https://api.dispatch.gumza.keEvery path below is relative to that host. If your operator runs their own deployment, swap the host and nothing else changes.
Authentication
Send your key in the X-API-Key header on every request. The key has the form <keyId>.<secret>; send it whole, exactly as issued.
X-API-Key: ebx_7f3c91a2.9Xq2vK8mPzR4tL6nW1yB3dH5sA7cF0jEThe key identifies your merchant account. A missing, malformed, revoked or suspended key is a 401 with code UNAUTHORIZED. There is no OAuth dance and no token to refresh.
Idempotency
Every request under /api/v1/external/dispatches requires an Idempotency-Key header. A UUID is the right thing to send.
- Replaying a key with the same body returns the original response verbatim, without creating a second delivery.
- Replaying a key with a different body is a
409with codeCONFLICT— that combination is always a bug on the caller side. - Keys are scoped to your merchant and expire after 24 hours.
- Omitting the header is a
422with codeVALIDATION_ERROR.
So: generate one key per logical write, store it alongside your order, and reuse it when you retry after a timeout. Use a fresh key for ordinary reads rather than reusing one, or you will be served a cached copy of an earlier response.
Endpoints
- POST
/api/v1/external/pingVerify the key and mark your integration connected - GET
/api/v1/external/quotesFee, distance and ETA for a pickup/dropoff pair - POST
/api/v1/external/dispatchesCreate a delivery - POST
/api/v1/external/dispatches/bulkCreate up to 500 deliveries in one transaction - GET
/api/v1/external/dispatchesList deliveries, filtered and cursor-paginated - GET
/api/v1/external/dispatches/:idRead one delivery - GET
/api/v1/external/dispatches/:id/eventsThe full status history for a delivery - GET
/api/v1/external/dispatches/:id/webhooksWebhook delivery attempts for a delivery - PATCH
/api/v1/external/dispatches/:id/statusCancel a delivery
Quote a delivery
Optional but worth doing: it tells you the fee before you commit, and the SLA targets for the zone the pickup falls in, so you can warn a customer that a delivery is already at risk of being late.
curl -G https://api.dispatch.gumza.ke/api/v1/external/quotes \
-H "X-API-Key: $DISPATCH_API_KEY" \
--data-urlencode "pickup_lat=-1.2672" \
--data-urlencode "pickup_lng=36.8065" \
--data-urlencode "dropoff_lat=-1.2833" \
--data-urlencode "dropoff_lng=36.7833"
# → {
# "success": true,
# "data": {
# "fee_cents": 32000,
# "currency": "KES",
# "zone_id": "0b1e…",
# "zone_name": "Nairobi Central",
# "distance_meters": 4120,
# "eta_travel_seconds": 944,
# "target_pickup_seconds": 1800,
# "target_delivery_seconds": 5400
# }
# }The fee comes from the zone the pickup point falls inside, using that zone's per-kilometre schedule. Outside every zone, platform defaults apply. The ETA is a modelled estimate — straight-line distance, a detour factor, and an average speed — not a routed time, so treat it as a planning number rather than a promise.
Create a delivery
| Field | Type | Description |
|---|---|---|
| customerName | string required | Who the rider is delivering to. 1–200 characters. |
| customerPhone | string required | The number the rider calls on arrival. 7–20 characters; send it in international form. |
| pickup | object required | Where the rider collects. address is required; lat, lng and notes are optional. |
| dropoff | object required | Same shape as pickup. Where the parcel is going. |
| paymentType | 'COD' | 'PREPAID' required | COD means the rider collects payment on delivery and it is reconciled against the amount below. |
| externalId | string optional | Your own order id. Returned on every read and every webhook, so you never have to store ours. |
| amountCents | integer optional | Order value in minor units. Defaults to 0. Required in practice for COD. |
| currency | string optional | Three-letter code. Defaults to KES. |
| targetPickupAt | ISO 8601 optional | When you want it collected by. Used for SLA reporting. |
| targetDeliveryAt | ISO 8601 optional | When you want it delivered by. |
Coordinates are optional on both ends. With them, the delivery is matched to a zone, priced from that zone and routed; without them the rider navigates by the address text and the fee falls back to defaults. Send coordinates whenever you have them.
curl -X POST https://api.dispatch.gumza.ke/api/v1/external/dispatches \
-H "X-API-Key: $DISPATCH_API_KEY" \
-H "Idempotency-Key: 9b2f1c84-4e1a-4a7f-9f0e-2d3c5a6b7c8d" \
-H "Content-Type: application/json" \
-d '{
"externalId": "order_10294",
"customerName": "Customer 001",
"customerPhone": "+254700000001",
"pickup": { "address": "Shop 4, Westlands, Nairobi", "lat": -1.2672, "lng": 36.8065 },
"dropoff": { "address": "Kileleshwa, Nairobi", "lat": -1.2833, "lng": 36.7833,
"notes": "Gate 2, ask for reception" },
"paymentType": "COD",
"amountCents": 250000,
"currency": "KES",
"targetDeliveryAt": "2026-10-12T16:00:00.000Z"
}'{
"success": true,
"data": {
"id": "c0ffee11-2233-4455-6677-8899aabbccdd",
"externalId": "order_10294",
"status": "NEW",
"riderId": null,
"zoneId": "0b1e…",
"customerName": "Customer 001",
"customerPhone": "+254700000001",
"pickup": { "address": "Shop 4, Westlands, Nairobi", "lat": -1.2672, "lng": 36.8065 },
"dropoff": { "address": "Kileleshwa, Nairobi", "lat": -1.2833, "lng": 36.7833,
"notes": "Gate 2, ask for reception" },
"paymentType": "COD",
"amountCents": 250000,
"feeCents": 32000,
"currency": "KES",
"targetPickupAt": null,
"targetDeliveryAt": "2026-10-12T16:00:00.000Z",
"trackingToken": "Yk3n…",
"source": "api",
"createdAt": "2026-10-12T08:41:02.117Z",
"updatedAt": "2026-10-12T08:41:02.117Z"
}
}Store id if you want to call back about this delivery, and trackingToken if you want to give your customer a live link — see Tracking.
Bulk create
POST /dispatches/bulk takes { "orders": [ … ] } with up to 500 entries, each exactly the shape above. All rows are written in one transaction: if the batch fails, no deliveries are created. Each row is priced independently against its own zone.
Use it for a daily upload. For anything event-driven, create one at a time — you get a clearer failure and per-delivery idempotency.
Status lifecycle
NEW ──► ASSIGNED ──► PICKED_UP ──► IN_TRANSIT ──► DELIVERED
│ │ │ │
└──────────┴─────────────┴──────────────┴────────► FAILED
└──────────────────────────────────────────────► CANCELLED| Status | Means |
|---|---|
| NEW | Accepted and waiting for a rider |
| ASSIGNED | A rider has it and is heading to the pickup |
| PICKED_UP | Parcel collected |
| IN_TRANSIT | On the way to the customer |
| DELIVERED | Handed over, with proof of delivery attached |
| FAILED | Could not be completed; carries a reason code |
| CANCELLED | Called off before completion |
A failed delivery carries one of CUSTOMER_UNREACHABLE, WRONG_ADDRESS, REFUSED, DAMAGED, RIDER_UNAVAILABLE or OTHER, plus free-text detail from the rider.
Cancel a delivery
curl -X PATCH https://api.dispatch.gumza.ke/api/v1/external/dispatches/$ID/status \
-H "X-API-Key: $DISPATCH_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "status": "CANCELLED", "reason": "Customer cancelled the order" }'Cancel while it is still NEW where you can. Once a rider is carrying the parcel, cancelling is an operational decision rather than an API one — talk to your operator, and expect the delivery to end as FAILED with a reason instead.
Read a delivery back
Webhooks are the intended way to track progress; these reads are for reconciliation, a support screen, or recovering after an outage on your side.
| Field | Type | Description |
|---|---|---|
| status | string optional | Filter the list by one of the lifecycle statuses above. |
| source | 'api' | 'csv' | 'shopify' | 'staff' optional | Where the delivery came from. |
| limit | integer optional | 1–200. Defaults to 50. |
| cursor | string optional | From meta.nextCursor on the previous page. Absent means you have reached the end. |
GET /dispatches/:id/events returns the ordered history behind a delivery — every transition, when it happened and who caused it. It is the right thing to show on a support screen and the right thing to attach to a dispute.
GET /dispatches/:id/webhooks returns our attempts to notify you about that delivery: status, attempt count, the last HTTP code we got back and the last error. Check it first when your system has missed an event.
Next
Creating deliveries is the easy half. The half that decides whether your integration is any good is what happens when status changes — read Webhooks next, then Errors & limits.