---
title: 'Cut voice costs: migrate from Africa''s Talking to Sautikit in an afternoon'
description: >-
  Move your voice from Africa's Talking to Sautikit in five steps: claim a
  number, swap auth, keep your voice XML (or use the JSON DSL), verify webhooks.
summary: >-
  A short, practical migration path from Africa's Talking voice to Sautikit:
  claim a KES 116 number, swap the auth header, keep your voice XML (or use the
  JSON DSL), and update webhooks.
date: 2026-07-05T00:00:00.000Z
type: blog
---


## Summary

If you already run programmable voice on Africa's Talking (AT) and want to move it to Sautikit, this guide is the short version. Five steps: claim a number, swap the auth header, keep your voice XML (Sautikit accepts XML) and adjust a few verb differences, update webhook signature verification, and point your outbound calls at the new endpoint. Most teams finish a basic move in an afternoon.

## Why teams move

The pull is usually cost and self-serve speed:

- **Instant, self-serve numbers.** A Sautikit number is **KES 100/month ex VAT (KES 116 incl.)**, claimed instantly from the dashboard. AT's route to a DID starts around **KES 5,000 to get the number plus KES 2,500/month** incl. tax.
- **Keep your voice XML, or go JSON.** Sautikit accepts your existing voice XML as-is, and also offers a JSON DSL if you'd rather generate call flows from code and a database with typed, validated actions.
- **M-Pesa STK push top-up.** Fund your wallet in about 30 seconds: your phone rings with the PIN prompt, no Paybill menu.
- **Per-second billing.** Outbound is **KES 0.05/sec** billed per second after connect; inbound is free; 1 GB of recording storage is included.

None of this makes AT a bad platform; it is multi-channel and broad. If you only need voice in Kenya, Sautikit is the narrower, cheaper-to-start path.

## The migration path

### 1. Sign up and claim a number

Create a Sautikit workspace and claim a number from the dashboard: instant, **KES 116/month incl. VAT**, no setup fee. Via the API:

```bash
# Browse available numbers, then claim one
curl https://api.sautikit.com/v1/numbers/available \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"

curl -X POST https://api.sautikit.com/v1/numbers/{id}/claim \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"
```

### 2. Swap the auth header

AT sends an API key header. Sautikit uses a Bearer JWT, the same key on every request:

```bash
# Africa's Talking
-H "apiKey: <AT_API_KEY>"

# Sautikit
-H "Authorization: Bearer <SAUTIKIT_JWT>"
```

Store the JWT in your environment (`SAUTIKIT_API_KEY`) and update your HTTP client's default headers once.

### 3. Keep your voice XML (or use the JSON DSL)

Good news: this step is small. Sautikit accepts XML voice actions directly (it forwards the raw XML to the telephony engine), so your existing AT `<Response>` flows keep working. You don't port or rewrite anything to move over; you only adjust a couple of small verb differences.

Here is a "say a prompt, collect one digit" flow in **Sautikit's XML on the left and the same flow in JSON on the right, both accepted at runtime**. Two things change versus AT: the prompt `<Say>` **nests inside** `<GetDigits>` rather than sitting beside it, and the digit-count and callback attributes are `maxDigits`/`action` rather than AT's `numDigits`/`callbackUrl`. The JSON column is optional: reach for it only if you want Sautikit-side validation and typed SDKs; otherwise keep returning your XML.

```xml
<Response>
  <Say voice="alice" language="en-KE">Press 1 for account balance, or 2 to speak with an agent.</Say>
  <GetDigits maxDigits="1" timeout="5" finishOnKey="#">
    <Say>Press 1 for balance, 2 for an agent.</Say>
  </GetDigits>
</Response>
```

```json
{
  "actions": [
    {
      "say": {
        "text": "Press 1 for account balance, or 2 to speak with an agent.",
        "voice": "alice",
        "language": "en-KE"
      }
    },
    {
      "getDigits": {
        "numDigits": 1,
        "timeout": 5,
        "finishOnKey": "#",
        "nested": [
          { "say": { "text": "Press 1 for balance, 2 for an agent." } }
        ]
      }
    }
  ]
}
```

You can keep returning your XML from this handler unchanged. If you opt into the JSON DSL instead, your handler returns the JSON object:

```js
// Express: your voice_callback_url handler
app.post("/voice", (req, res) => {
  res.json({
    actions: [
      { say: { text: "Press 1 for account balance, or 2 to speak with an agent.", language: "en-KE" } },
      {
        getDigits: {
          numDigits: 1,
          timeout: 5,
          finishOnKey: "#",
          nested: [{ say: { text: "Press 1 for balance, 2 for an agent." } }],
        },
      },
    ],
  });
});
```

When the caller enters a digit, Sautikit POSTs back to your callback URL with a `Digits` field. The [Voice Actions reference](/developers/concepts/voice-actions) lists every verb (`say`, `play`, `getDigits`, `dial`, `record`, `redirect`, `reject`, `hangup`, `conference`, `stream`) in both XML and JSON; most map one-to-one from their AT XML equivalents.

### 4. Update webhook signature verification

Swap AT's verification for Sautikit's. Sautikit signs each webhook with HMAC-SHA256 over `body + "." + timestamp`, delivered in the `X-Sautikit-Signature: t=<ts>,v1=<hex>` header:

```js
import crypto from "node:crypto";

function verifySautikit(req, secret) {
  const header = req.get("X-Sautikit-Signature") || "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${req.rawBody}.${parts.t}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}
```

Reject any request that fails this check before acting on it.

### 5. Update the outbound call endpoint

Point outbound calls at `POST /v1/calls`. `to` is an array of E.164 strings; `from` must be a number your workspace owns:

```js
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: "+254712345678",
    to: ["+254700000001"],
    voice_callback_url: "https://yourapp.example/voice",
  }),
});
```

That is the full loop: claim, authenticate, return your voice actions (XML or JSON), verify the webhook, place the call.

## What about SMS, USSD, and airtime?

Sautikit is voice-only by design, so if your AT integration also uses SMS, USSD, WhatsApp, or airtime, those channels do not move to Sautikit; they move to [Helloduty](https://helloduty.com), the multi-channel CX platform Sautikit belongs to. Voice runs on Sautikit; SMS, WhatsApp, USSD, and the agent desk run on Helloduty, in the same family and the same billing relationship. So a full AT migration is usually voice → Sautikit and everything else → Helloduty, not a single swap.

## Get started

1. [Create a Sautikit workspace](/) and claim a number: instant, KES 116/month incl. VAT.
2. Top up over **M-Pesa STK push**: KES billing, no card.
3. Point one AT voice flow at Sautikit (your XML works as-is) and place a test call.

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