Build a Call Center with Conferences
Use the Conference verb to build a multi-leg call center queue. Place callers on hold, add agents, mute/unmute, and record conference sessions.
The Conference voice action places a call leg into a named room on the platform. Multiple legs (a waiting caller plus one or more agents) can join the same named room. You control the room lifecycle through statusEventsCallbackUrl, which receives join/leave/mute/unmute events. This guide walks through a complete call center pattern: queue caller, join agent, mute caller during transfers, and record the session.
When a call arrives at your number, place the caller into a conference room with startOnEnter: false. This puts the caller on hold (playing hold music) until an agent joins:
{
"actions": [
{
"say": {
"text": "Thank you for calling Acme support. Please hold while we connect you to an agent.",
"voice": "alice",
"language": "en-US"
}
},
{
"conference": {
"name": "support-queue-001",
"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"
}
}
]
}Key parameters:
startOnEnter: false: the conference does not begin playing audio until an agent joins. The caller hears hold music from waitUrl.endOnExit: true: when the agent leg leaves (hangs up), the conference ends automatically and the caller is disconnected.beep: false: no entry/exit beep for the caller in a call center setting (beeps are distracting).statusEventsCallbackUrl: your server receives conference lifecycle events here.statusEvents: the space-separated list of events you want delivered.Generate unique room names per call to isolate conversations:
function conferenceRoomName(callId) {
return `support-${callId.slice(-8)}`;
}When your system assigns an agent, dial the agent's number and join them to the same conference room. This is a separate outbound call:
curl -s -X POST "https://api.sautikit.com/v1/calls/originate" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+254722111111",
"from": "+254700000001",
"voice_callback_url": "https://ivr.example.com/agent-voice?room=support-queue-001"
}'Your agent voice callback joins the same named room with startOnEnter: true (so the conference starts when they join) and announces them:
app.post("/agent-voice", (req, res) => {
const roomName = req.query.room;
res.json({
actions: [
{
say: {
text: "You are now connected to a caller.",
voice: "alice",
language: "en-US",
},
},
{
conference: {
name: roomName,
startOnEnter: true, // start the conference when agent joins
endOnExit: true, // end conference when agent hangs up
beep: true, // beep so caller knows agent arrived
muted: false,
statusEventsCallbackUrl: "https://ivr.example.com/conference-events",
statusEvents: "start end join leave mute unmute",
record: true, // record from when agent joins
},
},
],
});
});The statusEventsCallbackUrl receives JSON events for the room. Use these to update your call center dashboard or trigger workflows:
app.post("/conference-events", express.json(), (req, res) => {
const { event, conference_name, caller, participants } = req.body;
switch (event) {
case "start":
console.log(`Conference ${conference_name} started`);
break;
case "join":
console.log(`${caller} joined ${conference_name} (${participants} in room)`);
// Notify your dashboard
notifyDashboard({ type: "caller_joined", room: conference_name, caller, count: participants });
break;
case "leave":
console.log(`${caller} left ${conference_name} (${participants} remaining)`);
if (participants === 0) {
console.log(`Conference ${conference_name} is empty`);
}
break;
case "mute":
console.log(`${caller} was muted in ${conference_name}`);
break;
case "unmute":
console.log(`${caller} was unmuted in ${conference_name}`);
break;
case "end":
console.log(`Conference ${conference_name} ended`);
break;
}
res.sendStatus(200);
});To transfer a caller from one agent to another without dropping the call, use a warm transfer pattern. First, dial a second agent into the same room. Then let agent 1 depart:
// Originate a second agent leg into the same room
async function warmTransfer(roomName, targetAgentNumber, fromNumber) {
await fetch("https://api.sautikit.com/v1/calls/originate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: targetAgentNumber,
from: fromNumber,
voice_callback_url: `https://ivr.example.com/agent-voice?room=${encodeURIComponent(roomName)}`,
}),
});
// Agent 2 joins; when agent 1 hangs up, the conference continues with the caller and agent 2.
}Because endOnExit: true is set on the agent leg (not the caller leg), the conference only ends when the last agent with endOnExit: true leaves. Set endOnExit: false on the incoming agent during a warm transfer so the room survives until agent 1 finishes the handoff.
To mute a participant (for example, to mute the caller while agents confer), include muted: true on that leg's Conference response:
{
"actions": [
{
"conference": {
"name": "support-queue-001",
"muted": true,
"startOnEnter": false
}
}
]
}Alternatively, if your platform PBX supports in-conference DTMF mute, enable collectDigits to let agents mute callers via keypad:
{
"conference": {
"name": "support-queue-001",
"collectDigits": true,
"numDigits": 1,
"finishOnKey": "",
"digitsCallbackUrl": "https://ivr.example.com/dtmf"
}
}Set record: true on the Conference verb to capture the entire session:
{
"conference": {
"name": "support-queue-001",
"record": true,
"startOnEnter": true,
"statusEventsCallbackUrl": "https://ivr.example.com/conference-events",
"statusEvents": "start end join leave"
}
}When the conference ends, a call.recording.ready event fires for each recorded call leg. Retrieve the recording the same way as a regular call recording:
curl -s -L \
"https://api.sautikit.com/v1/calls/$CALL_ID/recording" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" \
-o conference-recording.wavAfter a test call, retrieve the call records to confirm both legs completed:
# List recent calls
curl -s "https://api.sautikit.com/v1/calls?limit=5" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" | \
jq '.[] | {id, direction, status, duration_seconds}'Check that your conference status events were received by watching your server logs. You should see start, at least one join, and end events for each conference session.