---
title: >-
  Cut conference call lag in East Africa: low-latency bridges on a local Nairobi
  number
description: >-
  Set up a Sautikit conference bridge with a local Nairobi number, DTMF PIN
  entry, moderator controls, and recording. Full KES cost breakdown.
summary: >-
  A Nairobi local number routes through Safaricom's PoP at ~12 ms vs ~180 ms via
  a foreign bridge. This guide builds a PIN-gated conference room with recording
  and dial-out to Uganda.
date: 2026-07-03T00:00:00.000Z
type: blog
---


## Summary

Running distributed team calls across Nairobi, Kampala, and Lagos from a US-based conference bridge adds ~168 ms of unnecessary one-way latency for every Safaricom subscriber on the line. This guide sets up a Sautikit-hosted conference room with a local Nairobi dial-in number, DTMF PIN entry, moderator controls, and conference recording, all of it within East Africa's routing fabric and priced in KES.

## Why a local number changes the experience

When a Safaricom subscriber dials a US-based conference bridge, the call routes through Safaricom's core network, exits Kenya via the TEAMS or EASSy submarine cable, transits a US carrier's network, and finally terminates on the conference server. Each leg adds latency. The measured one-way delay on this path is typically 170–190 ms, comfortably above ITU-T G.114's 150 ms recommendation for natural conversation.

A Sautikit Nairobi number (KES 100/month ex. VAT, KES 116 incl. VAT) routes through Safaricom's Nairobi PoP directly to Sautikit's media infrastructure, also in Nairobi. Safaricom-to-Safaricom calls on Sautikit never leave Safaricom's core network. The measured one-way latency for this path is approximately 12 ms.

You can measure this yourself before committing. Send a SIP `OPTIONS` packet from your server to `sip.sautikit.com` and compare the round-trip time with the RTT to a US SIP endpoint:

```bash
# Measure SIP round-trip to Sautikit (Nairobi)
sngrep -I sip.sautikit.com -p 5060 --count 5

# For a quick UDP probe, sipsak works:
sipsak -s sip:ping@sip.sautikit.com -v
```

The difference in RTT directly translates to perceived audio delay. At 12 ms vs 180 ms, every participant on a Safaricom line notices.

## Claiming a Nairobi local number

Search the inventory for a Nairobi landline (`020` prefix) or a mobile `07xx` number:

```bash
curl -s "https://api.sautikit.com/v1/numbers/available?country=KE&prefix=020" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"
```

Every Sautikit number costs KES 100/month (ex. VAT; KES 116 incl. 16% VAT), whether it's a Nairobi `020` landline or a mobile `07xx` number. For a conference bridge, a landline number is still the better choice: it signals a dedicated service line and is easier to distribute in meeting invites without ambiguity about the carrier.

Claim the number and set a `voice_callback_url` pointing to your webhook handler:

```bash
curl -s -X POST \
  "https://api.sautikit.com/v1/numbers/{inventory_id}/claim" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

curl -s -X PATCH \
  "https://api.sautikit.com/v1/numbers/{number_id}/routing" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voice_callback_url": "https://your-server.example.com/conference/join",
    "voice_signing_secret": "your-webhook-secret"
  }'
```

## Designing the conference entry flow

The full entry flow is:

<!-- diagram omitted -->

When a caller dials in, Sautikit posts the call state to your `voice_callback_url`. Your handler returns a JSON action sequence: greet the caller, collect their PIN with `GetDigits`, validate it, and place them into the conference room.

```js
// conference/join endpoint: Node.js (Express)
import express from "express";

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

const CONFERENCE_ROOMS = {
  "1234": { name: "engineering-standup", moderatorPin: "9999" },
  "5678": { name: "all-hands",           moderatorPin: "0001" },
};

app.post("/conference/join", (req, res) => {
  // The webhook receives form fields when a call connects; digit
  // submissions arrive on the same URL with a `Digits` field set.
  const digits = req.body.Digits || "";

  if (!digits) {
    // First contact: ask for PIN
    return res.json({ actions: [
      { say: { text: "Welcome. Please enter your conference PIN followed by the hash key.", language: "en-KE" } },
      { getDigits: { numDigits: 4, timeout: 10000, finishOnKey: "#",
                     action: "https://your-server.example.com/conference/join" } },
    ] });
  }

  const room = CONFERENCE_ROOMS[digits];
  if (!room) {
    return res.json({ actions: [
      { say: { text: "Invalid PIN. Please try again.", language: "en-KE" } },
      { redirect: { url: "https://your-server.example.com/conference/join" } },
    ] });
  }

  // Valid PIN: place caller into conference
  return res.json({ actions: [
    { say: { text: `Joining ${room.name}. You are being connected.`, language: "en-KE" } },
    { conference: {
        name: room.name,
        muted: false,
        record: true,
        waitUrl: "https://your-server.example.com/conference/hold-music",
    } },
  ] });
});
```

The `conference` verb places the caller into a named room. Participants who dial in with the same `name` value share the same audio bridge. The room is created on first join and torn down when the last participant leaves.

## Recording the conference

Enable recording by setting `record: true` on the `conference` verb. Conference recording bills at KES 0.50/minute, but note that Sautikit bills **per participant-minute**, not per wall-clock minute.

For a 60-minute call with 10 participants: `60 min × 10 participants × KES 0.50 = KES 300` in recording costs. The first 1 GB of recording storage is free; the `tiers` above that start at KES 599/month for 10 GB.

The recording is attached to the call record for the conference leg. When the conference ends, Sautikit emits a `call.recording.ready` webhook event containing the call ID. Retrieve the presigned URL via `GET /v1/calls/{id}/recording`.

## Moderator controls

A participant can be designated moderator by setting `moderator: true` in the `conference` verb. Moderators can mute a specific participant, kick them, or end the conference via the commands endpoint at `POST /v1/conferences/{room_name}/commands`.

Mute a participant by their call ID:

```bash
curl -s -X POST \
  "https://api.sautikit.com/v1/conferences/engineering-standup/commands" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": "mute", "participant": "{call_id}"}'
```

Kick a participant:

```bash
curl -s -X POST \
  "https://api.sautikit.com/v1/conferences/engineering-standup/commands" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": "kick", "participant": "{call_id}"}'
```

End the conference and disconnect all participants:

```bash
curl -s -X POST \
  "https://api.sautikit.com/v1/conferences/engineering-standup/commands" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": "kill"}'
```

## Dialling out to remote participants

Rather than requiring remote participants to dial in, you can add them to the conference by dialling out from the bridge. This is useful for Ugandan or Tanzanian team members who would otherwise pay international rates.

Use the `Dial` verb with a `conference` destination to add a participant:

```bash
curl -s -X POST https://api.sautikit.com/v1/calls \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "from": "+254712345678",
    "to": ["+256700123456"],
    "voice_actions": {
      "actions": [
        {
          "say": {
            "text": "You are being added to the engineering standup. Please hold.",
            "language": "en-KE"
          }
        },
        {
          "conference": {
            "name": "engineering-standup"
          }
        }
      ]
    }
  }'
```

Dialling out to a Ugandan `+256` number from Sautikit costs KES 3/min per outbound leg, the same rate as any outbound call. When the participant picks up, Sautikit emits a `call.answered` webhook event for that outbound leg.

## Bandwidth envelope and codec selection

At 10 participants with Opus at 16 kbps (the default for WebRTC SIP clients), the conference mixer processes approximately 160 kbps of inbound audio simultaneously. For standard PSTN callers on Safaricom, the codec at the interconnect is G.711 PCMU (64 kbps); Sautikit transcodes to the internal mixing format at the media gateway. This transcoding happens transparently; you do not need to negotiate codecs in your webhook response.

The practical implication: participants calling from a Nairobi landline or Safaricom mobile will sound slightly different from participants on WebRTC clients. The G.711-to-Opus transcoding introduces approximately 5 ms of additional processing latency and a minor MOS drop from ~4.2 to ~3.9. For a team call this is imperceptible.

## Full cost breakdown: 60-minute 10-person call

| Line item | Calculation | KES |
|---|---|---|
| Number rental (monthly, prorated) | KES 100 / 30 days | ~3 |
| Inbound call legs (10 × 60 min × KES 0/min, free) | 600 participant-minutes | 0 |
| Conference recording | 10 × 60 × KES 0.50 | 300 |
| Dial-out to Uganda (1 participant × 60 min × KES 3/min) | 60 min | 180 |
| **Total** | | **483** |

For comparison, a subscription-based conferencing service at US$25/month at a KES 135/USD rate costs KES 3,375/month regardless of usage. The Sautikit model is pay-as-you-go: a team that holds two such calls per month pays approximately KES 966: well under that fixed cost, and in KES with a wallet you top up via M-Pesa.

## Hold music while waiting for participants

Supply a `waitUrl` on the `conference` verb pointing to an endpoint that returns a `play` action:

```js
app.post("/conference/hold-music", (req, res) => {
  return res.json({ actions: [
    { play: { url: "https://assets.your-server.example.com/hold.mp3", loop: 99 } },
  ] });
});
```

The `waitUrl` is polled while the participant waits for the first other participant to join. Once a second participant joins, the normal conference audio starts. The `play` loop terminates automatically when the conference begins.

## Get started

1. [Create a Sautikit workspace](/) and claim a phone number.
2. Top up over **M-Pesa**: KES billing, no card.
3. Point your number's `voice_callback_url` at a `/conference/join` handler and host your first PIN-gated bridge.

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

Need to notify participants over SMS or WhatsApp before the call, or run a staffed support desk alongside the bridge? [Helloduty](https://helloduty.com) adds those channels on top of Sautikit's voice.

## Next steps

- [Voice action verbs reference: Conference, Dial, Record](/developers/concepts/voice-actions)
- [Outbound call API](/developers/api#calls)
- [Pricing and KES wallet model](/pricing)
