---
title: 'Ship voice faster and bill in KES: a Twilio alternative for developers'
description: >-
  A developer's Twilio alternative for voice: JSON voice-actions vs TwiML, KES
  billing over M-Pesa, instant numbers, and a Media Streams equivalent.
summary: >-
  Compare Sautikit and Twilio for programmable voice: JSON voice-actions vs
  TwiML, KES + M-Pesa vs USD cards, instant KES-116 numbers, per-second billing,
  and a Stream media fork for realtime AI.
date: 2026-06-25T00:00:00.000Z
type: blog
---


If Twilio feels too USD-centric and too heavy for a focused voice build, there is a leaner path. Sautikit gives you programmable voice with JSON voice-actions, KES-native billing, and instant numbers, without leaving realtime-AI builders behind.

> **TL;DR**
> - Sautikit answers with a plain **JSON voice-actions** response **or your existing XML** (accepted and forwarded byte-for-byte) and bills in **KES over M-Pesa**: no FX, no card required.
> - A working number costs **~KES 116 and activates instantly**, versus Twilio's per-number pricing and USD wallet.
> - The `<Stream>` media fork is a **direct equivalent of Twilio Media Streams**, so realtime voice-AI builds are fully covered.

## Where each one wins

Twilio is the right call when you need dozens of countries, deep SDK coverage across many languages, and a mature ecosystem of add-ons. That global reach and breadth are real advantages.

Sautikit is the right call when your voice traffic is Kenya-first (and expanding), you want to bill and settle in KES without an FX line item, and you prefer a small, sharp API over a sprawling console. If you also need SMS, WhatsApp, USSD, or an agent desk, [Helloduty](https://helloduty.com) covers those channels while Sautikit handles voice.

## Voice logic: JSON voice-actions vs TwiML

Twilio drives call flow with TwiML, an XML dialect (`<Say>`, `<Gather>`, `<Dial>`). Sautikit's `voice_callback_url` is JSON-native but also accepts XML, so your webhook can answer with an `actions` array or return raw XML directly.

```js
import express from "express";

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

app.post("/voice", (req, res) => {
  res.json({
    actions: [
      { say: { text: "Karibu. Press 1 for sales.", language: "en-KE" } },
      {
        getDigits: {
          numDigits: 1,
          timeout: 5,
          finishOnKey: "#",
          action: "/voice/handle",
          nested: [{ say: { text: "Enter your choice.", language: "en-KE" } }],
        },
      },
    ],
  });
});

app.listen(3000);
```

If you are coming from TwiML, both forms below work at runtime: return JSON or XML, whichever fits your codebase. Sautikit parses and validates the JSON DSL; the XML is forwarded to the telephony engine byte-for-byte, so you can keep your TwiML muscle memory:

```xml
<Response>
  <Say language="en-KE">Karibu. Press 1 for sales.</Say>
  <GetDigits maxDigits="1" timeout="5" finishOnKey="#">
    <Say language="en-KE">Enter your choice.</Say>
  </GetDigits>
</Response>
```

```json
{
  "actions": [
    { "say": { "text": "Karibu. Press 1 for sales.", "language": "en-KE" } },
    {
      "getDigits": {
        "numDigits": 1,
        "timeout": 5,
        "finishOnKey": "#",
        "action": "/voice/handle",
        "nested": [{ "say": { "text": "Enter your choice.", "language": "en-KE" } }]
      }
    }
  ]
}
```

Two migration gotchas: `<GetDigits>` nests its prompt `<Say>` inside the element (it is not a separate action), and XML uses `maxDigits` where JSON uses `numDigits`.

The verbs map cleanly to TwiML: `say` ↔ `<Say>`, `getDigits` ↔ `<Gather>` / `<GetDigits>`, `dial` ↔ `<Dial>`, `play` ↔ `<Play>`, `record` ↔ `<Record>`, `conference` ↔ `<Conference>`, `redirect`, `reject`, and `hangup`. Webhook form fields include `Digits` and `From`, so your handler reads them the same way you would a Twilio callback. Full reference: [/developers/concepts/voice-actions](/developers/concepts/voice-actions).

## Billing: KES + M-Pesa vs USD card-only

Twilio runs on a USD wallet funded by card, which means every Kenyan shilling you spend passes through an FX conversion and card fees. Sautikit's wallet is prepaid **in KES**, topped up with an M-Pesa STK push. No card on file.

```js
await fetch("https://api.sautikit.com/v1/topups", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ provider: "mpesa", amount: 1000, phone: "+254700000000" }),
});
```

Outbound voice is **KES 3.00/min**, billed per second from the moment the call connects. Inbound is **free (KES 0)**. See [/pricing](/pricing) for the source of truth. Per-second billing matters for IVR and OTP flows where calls are short; you are not rounded up to the minute.

## Numbers: instant KES-116 vs per-number pricing

Claiming a Sautikit number costs **from KES 100/month ex VAT (KES 116 incl. VAT)** and provisions instantly. That is the sharpest contrast with legacy providers: a working number for the price of lunch, live in seconds, no procurement ticket.

> 
> 
> Recording is included (1 GB free), so you can turn on `record` in a `dial` action without a separate storage add-on.
> 
> 

## Realtime AI: `<Stream>` is the Media Streams equivalent

If you build voice agents, the question is always: can I fork live audio to my own model? Yes. Sautikit's `<Stream>` verb forks call audio to a WebSocket as raw binary PCM, the direct counterpart to Twilio Media Streams. Return it as raw XML:

```xml
<Response>
  <Stream name="agent" url="wss://your.ws/audio" track="both_tracks" outputSamplingRate="16000" statusEvents="stream-started stream-stopped stream-error"/>
</Response>
```

It is bidirectional: your WebSocket server plays audio back into the call by sending binary PCM frames on the same socket. The server must accept the `audio.drachtio.org` subprotocol.

```js
import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 8080, handleProtocols: () => "audio.drachtio.org" });

wss.on("connection", (ws) => {
  ws.on("message", (frame, isBinary) => {
    if (isBinary) {
      // raw PCM in: pipe to your STT / model
      // send PCM back on the same socket to speak into the call
      ws.send(synthesize(frame));
    }
  });
});
```

Status events carry `callSessionState` (`StreamStarted`, `StreamStopped`, `StreamError`) plus a `streamSid`, so you can track the fork lifecycle the way you would Media Streams' start/stop/media messages.

## Migration example: place a call

The most common first migration step is replacing Twilio's `client.calls.create(...)`. On Sautikit it is a single POST: no SDK required, just global `fetch`.

```js
const res = await fetch("https://api.sautikit.com/v1/calls", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    from: "+254700000000",
    to: ["+254711111111"],
  }),
});

const call = await res.json();
// 202 => { call_id, pbx_session_id: "HD_...", status: "ringing", stream_url }
console.log(call.call_id, call.status);
```

Then poll `GET /v1/calls/{id}` or subscribe to `call.answered` / `call.completed` webhooks (status `answered`, `no-answer`, `busy`, or `failed`). If your wallet is empty you get `402 wallet.insufficient_balance`; top up over M-Pesa and retry with the same `Idempotency-Key`.

## FAQ

### Is Sautikit a drop-in replacement for Twilio?
Closer than you'd expect. Sautikit accepts raw XML at runtime and forwards it to the telephony engine byte-for-byte, so you can keep returning your TwiML-style flows; the main change is swapping the SDK for plain HTTP. If you'd rather build call flows in code, the JSON voice-actions DSL is there too (Media Streams map to `<Stream>`). Most single-region voice apps migrate in an afternoon.

### Do I need Twilio's country coverage?
If you dial across many countries, Twilio's global footprint is a genuine reason to stay. Sautikit is Kenya-first and expanding, so it fits teams whose voice traffic is concentrated where we operate today.

### Can I still build realtime voice AI?
Yes. The `<Stream>` verb forks live PCM audio to your WebSocket and accepts PCM back on the same socket, which is the same building block as Twilio Media Streams for STT, LLM, and TTS pipelines.

### How is billing different?
Sautikit bills in KES from a prepaid wallet funded by M-Pesa, with per-second voice and free inbound. There is no USD conversion and no card requirement.

### What about SMS or WhatsApp?
Sautikit is voice-focused by design. For SMS, WhatsApp, USSD, or an agent desk alongside voice, use [Helloduty](https://helloduty.com), the multi-channel platform Sautikit is part of.

## Get started

1. [Create a Sautikit workspace](/) and claim a number (from KES 116, instant).
2. Top up over **M-Pesa**; no card required.
3. Point one Twilio call flow at Sautikit: keep its TwiML (Sautikit accepts XML) or express it as a JSON `actions` response, then set your number's `voice_callback_url` to it.

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

## Further reading

- [Sautikit vs Twilio in Africa: the cost angle](/compare/twilio)
- [Place your first call in 5 minutes](/blog/place-your-first-call)
- [Voice actions reference](/developers/concepts/voice-actions)
