Build a Call Center with Conferences
Build a call center on Sautikit with the conference voice action: hold callers in a room, join agents, transfer and record. Agent calls: KES 3.00 a minute.
Build a call center on Sautikit with the conference voice action: hold callers in a room, join agents, transfer and record. Agent calls: KES 3.00 a minute.
The conference voice action in Sautikit places a call into a named room. A waiting caller and one or more agents join the same room by name, and your server decides who joins when. The caller's incoming call is free, and each agent leg is an ordinary outbound call at KES 3.00 a minute, billed per second after the agent answers.
voice_callback_url. See Claim and route a number.When a call arrives, reply with a conference that has startOnEnter: false. The caller hears the audio at waitUrl until an agent joins. Give each call its own room name:
app.post("/voice", express.json(), (req, res) => {
if (req.body.callSessionState === "Completed") return res.json({ actions: [] });
const room = `support-${req.body.sessionId}`;
assignAgent(room); // your own logic; see Step 2
res.json({
actions: [
{ say: { text: "Thank you for calling. Please hold while we connect you to an agent." } },
{
conference: {
name: room,
startOnEnter: false,
endOnExit: true,
beep: false,
waitUrl: "https://cdn.example.com/hold-music.mp3",
statusEventsCallbackUrl: "https://ivr.example.com/conference-events",
statusEvents: "start end join leave mute unmute",
},
},
],
});
});startOnEnter: false: the room does not start until someone with startOnEnter: true joins.endOnExit: true: the room ends when this participant leaves, so an agent is never left alone in it.waitUrl: hold audio, as a public MP3 or WAV URL.statusEventsCallbackUrl and statusEvents: where Sautikit posts room events, and which ones (space-separated).When your system picks an agent, place an outbound call to them. to is an array of strings:
async function callAgentIntoRoom(agentNumber, room) {
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: "+254720000000", // your Sautikit number
to: [agentNumber],
voice_callback_url: `https://ivr.example.com/agent-voice?room=${encodeURIComponent(room)}`,
}),
});
}When the agent answers, your callback puts them in the same room with startOnEnter: true:
app.post("/agent-voice", express.json(), (req, res) => {
if (req.body.callSessionState === "Completed") return res.json({ actions: [] });
res.json({
actions: [
{ say: { text: "You are now connected to a caller." } },
{
conference: {
name: req.query.room,
startOnEnter: true, // the room starts when the agent joins
endOnExit: true, // and ends when the agent hangs up
beep: true, // the caller hears the agent arrive
record: true,
statusEventsCallbackUrl: "https://ivr.example.com/conference-events",
statusEvents: "start end join leave mute unmute",
},
},
],
});
});The call to the agent is an ordinary outbound call: KES 3.00 a minute, billed per second after the agent answers. The caller's incoming call is free.
Sautikit POSTs a JSON body to statusEventsCallbackUrl for each event you asked for. Use them to update your wallboard:
app.post("/conference-events", express.json(), (req, res) => {
console.log("conference event", req.body);
res.sendStatus(200);
});Log a few real events from a test call before you build on the field names.
To move a caller from one agent to another without dropping the call, call the second agent into the same room, then let the first agent leave:
await callAgentIntoRoom("+254720000001", room);An agent with endOnExit: true ends the room when they hang up. For a transfer, return endOnExit: false for the first agent so the room survives their exit, and keep endOnExit: true on the agent who takes over.
To put a participant in the room muted, set muted: true on their conference action:
{
"actions": [
{
"conference": {
"name": "support-HD_9f2c1a7b",
"muted": true
}
}
]
}To mute or unmute someone who is already in the room, send a command with POST /v1/conferences/{room_name}/commands:
await fetch(`https://api.sautikit.com/v1/conferences/${encodeURIComponent(room)}/commands`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ command: "mute", participant: participantId }),
});The same endpoint takes unmute, list (who is in the room), say, play, exit (take one participant out without ending their call) and kill (end the room).
Set record: true on the conference action, as in Step 2. Making a recording is free; it uses your recording storage. Fetch it by call id once the call has ended:
curl -s -L \
"https://api.sautikit.com/v1/calls/$CALL_ID/recording" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" \
-o conference-recording.wavThe download link behind that request lasts 15 minutes, so request a fresh one each time. Record and stream to S3 shows how to copy recordings to your own bucket.
After a test call, list the recent calls. You should see the caller's inbound call and the outbound call to the agent, both completed:
curl -s "https://api.sautikit.com/v1/calls?limit=5" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" | \
jq '.data[] | {id, direction, status, duration_seconds}'Your server log should show a start, at least one join, and an end event for the room.