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 receives the call, checks agent availability, and either dials an agent directly or drops the caller into a hold conference room until an agent is ready. 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.
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.
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:
Say: queue position announcements, hold messages.Play: hold music audio file.Dial: connect caller to an agent's number or SIP URI.Conference: hold queue and agent bridge room.GetDigits: optional IVR pre-routing (department selection).Redirect: re-route a call while it is in progress.Hangup: end the call, update your CRM status.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);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"
}'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.
Conference hold time: the inbound leg continues to accrue per-minute billing while the caller sits in a hold conference waiting for an agent. Keep hold times short to control cost.
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.