Skip to content
DocsProductSign inCreate an account

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

Production
https://api.dispatch.gumza.ke

Every 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.

Every request
X-API-Key: ebx_7f3c91a2.9Xq2vK8mPzR4tL6nW1yB3dH5sA7cF0jE

The 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 409 with code CONFLICT — 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 422 with code VALIDATION_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.

GET /api/v1/external/quotes
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

FieldTypeDescription
customerNamestring
required
Who the rider is delivering to. 1–200 characters.
customerPhonestring
required
The number the rider calls on arrival. 7–20 characters; send it in international form.
pickupobject
required
Where the rider collects. address is required; lat, lng and notes are optional.
dropoffobject
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.
externalIdstring
optional
Your own order id. Returned on every read and every webhook, so you never have to store ours.
amountCentsinteger
optional
Order value in minor units. Defaults to 0. Required in practice for COD.
currencystring
optional
Three-letter code. Defaults to KES.
targetPickupAtISO 8601
optional
When you want it collected by. Used for SLA reporting.
targetDeliveryAtISO 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.

POST /api/v1/external/dispatches
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"
  }'
201 Created
{
  "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

Happy path, and the ways out of it
NEW ──► ASSIGNED ──► PICKED_UP ──► IN_TRANSIT ──► DELIVERED
 │          │             │              │
 └──────────┴─────────────┴──────────────┴────────► FAILED
 └──────────────────────────────────────────────► CANCELLED
StatusMeans
NEWAccepted and waiting for a rider
ASSIGNEDA rider has it and is heading to the pickup
PICKED_UPParcel collected
IN_TRANSITOn the way to the customer
DELIVEREDHanded over, with proof of delivery attached
FAILEDCould not be completed; carries a reason code
CANCELLEDCalled 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

PATCH /api/v1/external/dispatches/:id/status
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.

FieldTypeDescription
statusstring
optional
Filter the list by one of the lifecycle statuses above.
source'api' | 'csv' | 'shopify' | 'staff'
optional
Where the delivery came from.
limitinteger
optional
1–200. Defaults to 50.
cursorstring
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.