SautiKit
/ai-agents/pricing/docs/api/blog
sign inStart building

Run a call center without the platform: inbound queues and outbound dialling

Build an inbound queue and outbound dialler for a call center using the Queue verb, Dial, and voice-action webhooks.

use-casecall-centerqueuedialrouting

Next Steps

  • QueueQueueAction holds the caller in one of your queues until an agent answers, the caller hangs up, or maxWaitTime is reached. Queue is not a terminal verb. Actions after it run when the caller leaves
  • DialDialAction connects the caller to one or more numbers/SIP endpoints. Number/SIP URIs are subject to the same destination authorisation as POST /v1/calls. URL must be on the allow-list when dialling
  • GetDigitsGetDigitsAction collects DTMF from the caller, optionally with a nested `<Say>` / `<Play>` prompt. Mirrors the PBX `<GetDigits>` verb.
  • CallsEvery phone call in Sautikit is a call record with a direction (inbound or outbound), a sequence of lifecycle states, and an optional recording. Cost is debited at hangup based on answered duration.
SautiKit

The Voice Kit for Africa. Buy numbers, build call flows, and pay per second in local currency.

All systems operational

Product

AI voice agentsBroadcastsSautiKit Phone appsNumbersCalls & routingRecordingsWallet & billingPricing

Developers

DocumentationAPI referenceVoice actionsWebhooksErrorsMCP serverQuickstartAI prompt

Compare

vs Africa's Talkingvs Twiliovs Infobipvs 3CXvs Vapi, Retell & BlandMigrate from Africa's TalkingAll comparisons

Company

AboutBlogConsole

© 2026 Sautikit. All rights reserved • Powered by Helloduty

Terms of ServicePrivacy Policy

Sautikit provides voice API services for application developers. Numbers provisioned on this platform are not configured for emergency calling (e.g. 999 / 112). Do not use Sautikit numbers as a replacement for a primary phone line.

Summary

A call center on Sautikit is a combination of inbound call routing and outbound dialling, both controlled by voice-action webhooks. For inbound calls, your webhook returns a Queue action and the phone switch's own call-centre queue takes over: it holds the caller, announces their position, picks which agent to offer the call to, and rings them. For outbound calls, your server initiates a POST /v1/calls and uses a webhook to connect the answered call to an agent. No proprietary call center platform is required; the logic lives in your application.

Who this is for

  • Companies building or replacing a call center software layer on top of programmable telephony.
  • Development teams that need to integrate call routing with an existing CRM or ticketing system.
  • Startups that want a lightweight inbound queue and callback flow without a full contact center platform.
  • Teams building outbound dialling campaigns for sales, collections, or appointment reminders.

How it works

Inbound queue

Return a Queue action and the phone switch's own call-centre queue takes the caller: it holds them, announces their position, chooses which of your agents to offer the call to, and rings that agent's device. Your server decides nothing per-call. Actions you place after the Queue run when the caller leaves it, which is where voicemail or a callback offer goes.

Inbound routing you assemble yourself

When the assignment decision has to be yours — a CRM lookup, a shift roster, a skills matrix — skip the queue and hold the caller in a named conference room instead, dialling an agent in when your own worker picks one:

Inbound queue flow
Inbound call center queue flowCustomer dials → routing webhook → check agent availability. If agent free: dial agent directly. If no agent: hold in conference room. Agent joins → both talk.Customer dials Sautikit numberPOST to routing webhookCheck agent availabilityAgent available?yes →Dial agent directly→ noHold in ConferencestartOnEnter: falseAgent + Customer talking
If no agent is available, the customer waits in a named conference hold room. When an agent frees up, your server dials them out and joins them to the same room.

Outbound campaign

Outbound call flow
Outbound call flowPOST /v1/calls/originate → platform dials remote party → POSTs to voice_callback_url on each step → call.hangup with wallet debit.POST /v1/calls/originate{ "to": "+254722000001", "from": "+254700000001", "voice_callback_url": "…" }Platform dials the remote partyPOST {voice_callback_url} on each stepcall.hangup → cost debited from wallet

Agent SIP endpoints

Agents can be reached three ways: by ordinary phone number, by SIP URI to your own PBX (sip:agent@yourpbx.example.com), or by registering each agent's softphone directly to Sautikit as a SIP credential.

The third is usually the one you want. Each registered device gets a four-digit internal extension, so an agent is reachable as 1001 from a number's forward target or from another agent's handset — and the agent-side leg costs nothing, because it never touches a carrier. From your own Dial verb, target the device by its credential identity (client:<username>), which the credential list returns.

That changes the arithmetic of a call centre. The usual model bills you twice for one conversation: once for the customer's leg and again for the leg out to whichever number the agent is sitting at. With agents registered to Sautikit you pay for the customer leg only.

Supervisors on live calls

A team lead can join a live agent call to listen silently, whisper coaching that only the agent hears, or barge into the conversation, and switch between the three without redialling. The supervisor joins from a browser, a SIP phone or their mobile; a browser or SIP supervisor costs KES 0.50 a minute. See Call monitoring, whisper and barge and the developer guide.

API surface

Endpoints you call:

  • POST /v1/calls: place an outbound call (to contact or to agent).
  • GET /v1/calls/{call_sid}: retrieve call metadata, duration, and status.
  • GET /v1/calls: list calls for reporting and queue dashboards.

Voice actions used:

  • Queue: park the caller until an agent is free. This is the inbound queue.
  • Say: greetings and hold messages.
  • Play: hold music audio file.
  • Dial: connect a caller straight to an agent's extension, number, or SIP URI.
  • GetDigits: optional IVR pre-routing (department selection).
  • Redirect: re-route a call while it is in progress, including on the way out of a queue.
  • Conference: bridges you assemble yourself, such as a hold room or a scheduled call.
  • Hangup: end the call, update your CRM status.

Example

Inbound routing webhook

The short version is one verb. Return a queue action and the switch does the waiting; put whatever should happen to an unanswered caller after it, because actions following a Queue run when the caller leaves it, however that ended:

import express from "express";
 
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
 
app.post("/calls/inbound", (req, res) => {
  res.json({
    actions: [
      { say: { text: "Thank you for calling Acme support." } },
      {
        queue: {
          name: "support",
          // A stable, cacheable URL. A freshly-signed URL per call is a new
          // cache key every time, and the switch downloads the file before it
          // parks the caller.
          waitUrl: "https://cdn.example.com/hold-music.mp3",
          callerId: req.body.From,
          announceVoice: "en-GB-Standard-F",
        },
      },
      // Runs when the caller leaves the queue unanswered.
      { redirect: { url: "https://yourapp.example.com/calls/voicemail" } },
    ],
  });
});
 
app.listen(3000);

There is no hold ceiling: a caller waits until an agent answers, until the queue's configured maximum wait if you set one, or until they hang up. When an agent hangs up, the next caller in line reaches them in roughly 170ms.

If you would rather not write a callback at all, set the queue block on the number itself with PUT /v1/numbers/{id}/routing. That reference also covers sharing_mode — the ring order across your agent pool — and the 202 response you get when the configuration is saved but the switch could not be reached, which is an expected outcome rather than an error.

Routing to agents yourself

You do not have to use the queue. If your own system already decides who takes each call — because it reads a CRM, or a shift roster, or a skills matrix you maintain — keep that logic and use Dial and Conference directly. The pattern below dials a free agent, and otherwise parks the caller in a per-call conference room that a background worker joins an agent to:

import express from "express";
import { findAvailableAgent, enqueueCall, dequeueCall } from "./agent-store";
 
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
 
// Sautikit calls this when a customer dials your number
app.post("/calls/inbound", async (req, res) => {
  const callId = req.body.CallId;
  const agent = await findAvailableAgent();
 
  if (agent) {
    // Agent is free: dial directly
    agent.markBusy();
    return res.json({
      actions: [
        { say: { text: "Connecting you to an agent." } },
        {
          dial: {
            number: agent.phoneNumber,
            callerId: req.body.To,
            timeout: 30,
          },
        },
      ],
    });
  }
 
  // No agent: put caller in a named hold conference
  await enqueueCall(callId);
  return res.json({
    actions: [
      { say: { text: "All agents are currently with other customers. Please hold." } },
      {
        conference: {
          name: `queue-${callId}`,
          startOnEnter: false,
          waitUrl: "https://yourapp.example.com/hold-music",
          statusEventsCallbackUrl: "https://yourapp.example.com/calls/queue-events",
          statusEvents: "join leave end",
          endOnExit: true,
        },
      },
    ],
  });
});
 
// Your background queue worker calls this when an agent becomes available
app.post("/calls/connect-agent", async (req, res) => {
  const { customerCallId, agentNumber } = req.body;
 
  // Dial the agent; when they answer, join the customer's conference
  const agentCallResponse = 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({
      to: agentNumber,
      from: process.env.SAUTIKIT_NUMBER,
      action_url: `https://yourapp.example.com/calls/agent-join?queue=${customerCallId}`,
    }),
  });
 
  res.json({ ok: true });
});
 
// When the agent answers, join the customer's hold conference
app.post("/calls/agent-join", (req, res) => {
  const customerCallId = req.query.queue;
  return res.json({
    actions: [
      {
        conference: {
          name: `queue-${customerCallId}`,
          startOnEnter: true,
          endOnExit: true,
          beep: false,
        },
      },
    ],
  });
});
 
app.listen(3000);

Place an outbound campaign call

curl -X POST "https://api.sautikit.com/v1/calls" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to":         "+254711222333",
    "from":       "+254700000001",
    "action_url": "https://yourapp.example.com/calls/outbound-answer",
    "status_url": "https://yourapp.example.com/calls/outbound-status"
  }'

Pricing notes

A call center workload involves multiple call legs per interaction:

  • Inbound customer leg: billed per minute from answer to hangup.

  • Outbound agent dial leg: each Dial or POST /v1/calls to an agent number is a separate outbound call billed at the destination rate.

  • Hold time: the inbound leg continues to accrue per-minute billing while the caller waits, whether in a queue or a hold conference. Nothing cuts a queued caller off, so a deep queue is a real line item — set a maximum wait and an overflow to voicemail if you would rather pay for the message than the wait.

  • Agent leg, when the agent is registered to Sautikit: not billed at all. A call bridged to a registered device by its extension consumes no carrier minutes, so there is nothing to charge. Only the customer's leg is billed.

Agents reached via a sip: URI to your own PBX are billed at the SIP termination rate rather than the mobile/landline rate — lower than dialling a mobile, but not free, because the call still leaves Sautikit to reach your PBX.

For outbound campaigns, factor in answer rates. Only answered calls proceed to the action_url and get connected to an agent. Unanswered calls (no-answer, busy) are billed only for the ring duration, typically 20–40 seconds.

Next steps

  • Queue voice action reference: hold audio, caller ID, and the announcement voice.
  • Bind a number's routing: the queue block, sharing_mode, and queue provisioning.
  • Dial voice action reference: caller ID, timeout, and SIP dialling.
  • Conference voice action reference: hold queue setup, statusEvents.
  • Conference calling use case: multi-party bridges.
  • Call monitoring, whisper and barge: supervisors on live calls.
  • Calls concept: call lifecycle and SID references.
  • IVR use case: add a department-selection menu before routing.