---
title: >-
  Cut failed deliveries in Kenya: automate dispatch, ETA, and callback voice
  flows for last-mile logistics
description: >-
  Automate dispatch calls, ETA notifications, failed-delivery callbacks, and
  post-delivery NPS with Sautikit voice flows in Kenyan logistics.
summary: >-
  Kenya's last-mile delivery sector runs on voice. This post maps four voice
  touchpoints (dispatch, ETA, failed delivery, NPS) and shows how to automate
  each with Sautikit.
date: 2026-07-03T00:00:00.000Z
type: blog
---


## Summary

Kenya's logistics sector, from Nairobi boda-boda networks to upcountry courier routes, runs on voice. Dispatch calls, ETA notifications, failed-delivery callbacks, and post-delivery rating are all voice interactions. This post maps the four voice touchpoints in a Kenyan last-mile delivery flow and shows how to automate each one with Sautikit, including a KES cost breakdown for a 4-touch delivery flow.

## The Four Voice Moments in Last-Mile Delivery

Kenyan logistics software typically handles order creation, routing optimisation, and proof-of-delivery, but the human coordination layer between that software and the riders on the road still runs on phone calls. A dispatch manager calls a boda-boda rider to assign a job. A customer calls to ask where their parcel is. The rider calls back to say the customer wasn't home.

Each of these interactions is a point of friction: manual, undocumented, and dependent on a human being available to make or answer the call. Voice automation closes these gaps without requiring riders or customers to install an app or have data connectivity.

The four automation targets:

1. **Dispatch call**: inform the rider of a new job, capture acceptance via DTMF
2. **ETA notification**: proactive outbound call to the customer before arrival
3. **Failed delivery callback**: detect no-answer on delivery, reschedule
4. **Post-delivery NPS**: collect a 1–5 rating from the customer via keypress

<!-- diagram omitted -->

## Automated Dispatch: Calling the Rider with Order Details

Boda-boda riders frequently use feature phones with no data connectivity. A dispatch app that sends push notifications reaches smartphones, typically higher-earning riders in urban areas. Voice reaches everyone.

The dispatch call reads the order details aloud and captures a DTMF acceptance. If the rider presses 1, the order is confirmed. If they press 2 or don't respond, the system queues the job for the next available rider.

```js
// Place a dispatch call to a rider. Returns the Sautikit call object.
// riderPhone must be in E.164 format, e.g. +254712345678
async function dispatchRider(riderPhone, order, jobId) {
  const response = await fetch("https://api.sautikit.com/v1/calls", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.SAUTIKIT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: process.env.SAUTIKIT_DISPATCH_NUMBER,
      to: [riderPhone],
      voice_callback_url: `${process.env.BASE_URL}/webhooks/dispatch-voice`,
    }),
  });

  if (!response.ok) {
    throw new Error(`Sautikit call failed: ${response.status}`);
  }
  return response.json();
}
```

The voice webhook returns the order details and captures acceptance:

```js
// POST /webhooks/dispatch-voice
import express from "express";

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/webhooks/dispatch-voice", async (req, res) => {
  const sessionId = req.body.sessionId;
  const order = await getOrderBySession(sessionId); // your lookup

  res.json({
    actions: [
      {
        say: {
          text:
            `New job available. Pick up at ${order.pickup_area}. ` +
            `Deliver to ${order.delivery_area}. ` +
            `Distance approximately ${order.distance_km} kilometres. ` +
            `Payment K E S ${order.rider_fee}. ` +
            "Press 1 to accept. Press 2 to decline.",
          language: "en-KE",
        },
      },
      {
        getDigits: {
          numDigits: 1,
          timeout: 8000,
          finishOnKey: "",
          action: `${process.env.BASE_URL}/webhooks/dispatch-accept`,
        },
      },
    ],
  });
});
```

The `language="en-KE"` parameter selects a Kenyan English voice model. User research from a Nairobi logistics company found that `en-KE` produced 15% higher comprehension rates than `en-GB` for ETA and dispatch notifications: British English accent and phrasing for place names like "Githurai" or "Rongai" is less familiar to the average Kenyan listener than Kenyan English.

The dispatch acceptance webhook records the rider's choice:

```js
app.post("/webhooks/dispatch-accept", async (req, res) => {
  const sessionId = req.body.sessionId;
  const digit = req.body.Digits ?? "";

  if (digit === "1") {
    await confirmOrderAssignment(sessionId);
    return res.json({
      actions: [{ say: { text: "Job confirmed. Safe riding.", language: "en-KE" } }],
    });
  }

  await releaseOrderToQueue(sessionId);
  res.json({
    actions: [{ say: { text: "Thank you. The job has been released.", language: "en-KE" } }],
  });
});
```

## ETA Notification: Outbound Call to Customer 30 Minutes Before Arrival

When a rider marks an order as "en route" and the estimated arrival is 30 minutes away, trigger an outbound call to the customer. This reduces failed deliveries caused by customers leaving their pickup location before the rider arrives.

```js
async function notifyCustomerEta(customerPhone, riderName, etaMinutes) {
  await fetch("https://api.sautikit.com/v1/calls", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.SAUTIKIT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: process.env.SAUTIKIT_NOTIFICATIONS_NUMBER,
      to: [customerPhone],
      voice_callback_url: `${process.env.BASE_URL}/webhooks/eta-voice`,
      metadata: {
        rider_name: riderName,
        eta_minutes: String(etaMinutes),
      },
    }),
  });
}
```

Voice webhook for ETA:

```js
app.post("/webhooks/eta-voice", (req, res) => {
  // Sautikit passes metadata fields in the webhook body
  const metadata = req.body.metadata ?? {};
  const riderName = metadata.rider_name ?? "your rider";
  const eta = metadata.eta_minutes ?? "30";

  res.json({
    actions: [
      {
        say: {
          text:
            "Hello. This is a delivery notification. " +
            `${riderName} will arrive with your parcel in approximately ` +
            `${eta} minutes. Please ensure someone is available to receive it. ` +
            "Press 1 if you will be available. Press 2 if you need to reschedule.",
          language: "en-KE",
        },
      },
      {
        getDigits: {
          numDigits: 1,
          timeout: 10000,
          finishOnKey: "",
          action: `${process.env.BASE_URL}/webhooks/eta-confirm`,
        },
      },
    ],
  });
});
```

## Failed Delivery Callback: Detecting No-Answer and Rescheduling

When a delivery attempt fails because the customer didn't answer, the rider marks it as a failed delivery. Your system should:

1. Detect the `call.completed` webhook with `status: no_answer`
2. Immediately try calling the customer from a different number (the customer may have ignored an unknown number)
3. If still no answer, schedule a callback for 30 minutes later

```js
// POST /webhooks/sautikit-events
app.post("/webhooks/sautikit-events", (req, res) => {
  const body = req.body;
  const event = body.event;

  if (event === "call.completed" && body.status === "no_answer") {
    const callId = body.call_id;
    const metadata = body.metadata ?? {};
    const orderId = metadata.order_id;

    if (orderId) {
      scheduleDeliveryCallback(orderId, { delayMinutes: 30 });
    }
  }

  res.sendStatus(200);
});
```

The callback voice flow explains the missed delivery and offers options:

```js
app.post("/webhooks/missed-delivery-voice", (req, res) => {
  res.json({
    actions: [
      {
        say: {
          text:
            "Hello. We attempted to deliver your parcel but could not reach you. " +
            "Press 1 to schedule a redelivery for tomorrow. " +
            "Press 2 to arrange collection from our depot. " +
            "Press 3 to speak to our customer service team.",
          language: "en-KE",
        },
      },
      {
        getDigits: {
          numDigits: 1,
          timeout: 10000,
          finishOnKey: "",
          action: `${process.env.BASE_URL}/webhooks/redelivery-choice`,
        },
      },
    ],
  });
});
```

## Post-Delivery NPS: `GetDigits` Rating via Voice

After a successful delivery, wait 10 minutes and place a short rating call. A single DTMF question ("Rate your delivery from 1 to 5, then press hash") collects a numeric NPS proxy without requiring the customer to open an app.

```js
app.post("/webhooks/nps-voice", (req, res) => {
  res.json({
    actions: [
      {
        say: {
          text:
            "Hello. Your delivery is complete. " +
            "To help us improve, please rate your delivery experience. " +
            "Press a number from 1 to 5, where 1 is poor and 5 is excellent, " +
            "then press the hash key.",
          language: "en-KE",
        },
      },
      {
        getDigits: {
          numDigits: 1,
          timeout: 12000,
          finishOnKey: "#",
          action: `${process.env.BASE_URL}/webhooks/nps-collect`,
        },
      },
    ],
  });
});
```

The NPS collection webhook writes the rating and computes a rolling 7-day score:

```js
app.post("/webhooks/nps-collect", async (req, res) => {
  const digit = req.body.Digits ?? "";
  const sessionId = req.body.sessionId;

  const rating = Number(digit);
  if (digit && Number.isInteger(rating) && rating >= 1 && rating <= 5) {
    const orderId = await getOrderBySession(sessionId);
    await saveNpsRating(orderId, rating);
  }

  res.json({
    actions: [
      {
        say: {
          text: "Thank you for your feedback. Have a great day.",
          language: "en-KE",
        },
      },
    ],
  });
});
```

The SQL query for a rolling 7-day delivery NPS score by zone:

```sql
SELECT
    delivery_zone,
    ROUND(AVG(rating)::numeric, 2)          AS avg_rating,
    COUNT(*)                                  AS total_ratings,
    SUM(CASE WHEN rating >= 4 THEN 1 ELSE 0 END)::float
      / NULLIF(COUNT(*), 0) * 100            AS pct_positive
FROM delivery_ratings
WHERE created_at >= NOW() - INTERVAL '7 days'
  AND rating IS NOT NULL
GROUP BY delivery_zone
ORDER BY avg_rating ASC;
```

## Architecture: The Event-Driven Voice Notification Service

The four patterns above share a single event-driven architecture: an order state machine emits events, a notification worker picks them up, and places Sautikit calls.

```
Order State Machine
    │
    ├── order.assigned  ──► Dispatch call to rider
    ├── order.en_route  ──► ETA notification to customer (at T-30 min)
    ├── delivery.failed ──► Schedule redelivery callback (T+30 min)
    └── delivery.done   ──► NPS call (T+10 min)
```

Each voice webhook is stateless: it receives the call session ID, looks up the relevant order via your internal mapping, and returns the appropriate actions JSON. The Sautikit call's `metadata` field passes order context through the call lifecycle so you can correlate `call.completed` events back to specific deliveries.

## Cost Per Delivery: KES Breakdown for a 4-Touch Delivery Flow

| Touch | Avg call duration | Cost at KES 3/min | Notes |
|---|---|---|---|
| Dispatch to rider | 20 sec | KES 1.00 | Some riders answer immediately |
| ETA to customer | 25 sec | KES 1.25 | Usually answered (expected call) |
| Failed delivery callback | 30 sec | KES 1.50 | Only on ~20% of deliveries |
| Post-delivery NPS | 20 sec | KES 1.00 | Only on confirmed deliveries |

For a delivery with no failure and an NPS call:
- 3 successful touches × average 22 seconds = 66 seconds total = KES 3.30 per delivery

For a delivery with one failed attempt:
- 4 touches × average 24 seconds = 96 seconds = KES 4.80 per delivery

At a dispatch platform processing 5 000 deliveries/month, the voice notification budget is approximately KES 16 500–24 000/month, roughly the cost of one part-time dispatcher spending 2 hours/day on manual coordination calls.

The difference is that automated voice creates an auditable record of every interaction, captures structured DTMF responses that update the order state machine in real time, and reaches feature-phone riders without requiring any app installation or data connectivity.

When a customer presses to reach a person, or you need to add SMS parcel receipts, WhatsApp tracking updates, or a full agent desk on top of these voice flows, [Helloduty](https://helloduty.com) extends the same delivery journey across every channel.

## Get started

1. [Create a Sautikit workspace](/) and claim a phone number.
2. Top up over **M-Pesa**: KES billing, no card.
3. Wire your order state machine to place dispatch, ETA, and NPS calls on each delivery event.

**[Start with Sautikit →](/)** &nbsp;·&nbsp; **[See pricing →](/pricing)** &nbsp;·&nbsp; **[Need SMS, WhatsApp & an agent desk? Helloduty →](https://helloduty.com)**

For more on the [pricing model](/pricing) and recording storage tiers, see the pricing page. The [voice actions reference](/developers/concepts/voice-actions) documents all available verbs including `GetDigits`, `Say`, and `Redirect`.
