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 via SIP URI (sip:agent@yourpbx.example.com) or ordinary phone numbers. Using SIP avoids per-minute costs on agent-side minutes if your agents use a softphone on the same SIP trunk.
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:
Dial or POST /v1/calls to an agent number is a separate outbound call billed at the destination rate.Agents reached via SIP (sip: URIs to your own PBX or softphone) are billed at the SIP termination rate rather than the mobile/landline termination rate, which is typically lower for on-premises deployments.
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.