Practical Guide: Complete Call Center Agent
This guide walks you through building a complete call center agent that demonstrates all SIP voice tools working together. Unlike simple examples, this is a real-world implementation you can adapt for production use.
What We're Building
A customer service agent that:
- Greets callers and asks for identification
- Collects SSN via phone keypad (
receive_dtmf) for customer lookup - Resolves issues or...
- Transfers to human agents (
transfer_call) when needed, or... - Navigates external IVRs (
send_dtmf) to check other systems - Ends calls (
end_dialog) capturing structured outcome data
Integration Architecture
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Your PBX │───SIP INVITE───▶│ SipPulse AI │────▶│ Your CRM │
│ │ + headers │ │ │ │
│ call-id: │ │ │ │ │
│ abc123 │ x-uid: abc123 │ thread.uid = │ │ Fetch by │
│ │ x-additional- │ abc123 │ │ UID abc123 │
│ │ instructions: │ (auto-created)│ │ │
└─────────────┘ "caller: ..." └──────────────┘ └─────────────┘Key Integration Points:
- SIP headers pass UID and context—no API call needed before the call
- The system automatically creates the thread when the call connects
- After call ends, fetch thread by UID to extract outcomes
- Use
end_dialogdata to update your CRM/ticketing system
Quick Start with Template
Want to skip the manual setup? Go to Agents → Create Agent → Templates and select "SIP Call Center Agent". The template includes all configurations from this guide—just customize the transfer destinations and prompts for your business.
Prerequisites
Before starting:
- SipPulse AI account with API access
- SIP deployment configured (see SIP Integration guide)
- Basic understanding of agents (see Agent Introduction)
- A SIP phone or softphone for testing
Voice Playground Limitation
SIP voice tools only work during actual SIP calls. They cannot be tested in Voice Playground. You need a real SIP connection to test this guide.
Part 1: Agent Configuration
1.1 Create the Voice Agent
- Navigate to Agents and click Create Agent
- Select Voice Agent
- Name it: "Customer Service Agent"
- Choose a model with strong tool-calling: gpt-5-mini or claude-4-5-sonnet recommended
1.2 Write Agent Instructions
This prompt enables the agent to handle multiple scenarios and use all voice tools appropriately:
# Customer Service Agent - Acme Corp
You are the virtual customer service agent for Acme Corp. Your goal is to help
customers efficiently while maintaining a friendly, professional tone.
## Context Variables
You have access to these variables from the caller's session:
- {{caller_id}} - The caller's phone number
- {{inbound_did}} - Which company line they called
## Identification Flow
### Step 1: Greeting and ID Request
Greet the customer warmly and ask them to identify themselves:
"Please enter your 9-digit Social Security Number using your phone keypad. Press pound when done,
or star to clear and start over if you make a mistake."
### Step 2: Collect SSN (CRITICAL)
- Use receive_dtmf to collect exactly 9 digits
- Timeout: 30 seconds
### Step 3: Confirm the SSN (MANDATORY)
After receiving digits, you MUST:
1. Read back EVERY digit individually: "I received: one, two, three..."
2. Ask: "Is that correct?"
3. If NO: Apologize and collect again
4. If YES: Proceed to assistance
IMPORTANT: NEVER skip confirmation. NEVER read as a single number.
## Assistance Flow
After confirming identity:
1. Ask how you can help today
2. Try to resolve the issue directly
3. If you cannot resolve, transfer to the appropriate department
## When to Transfer
Transfer the call when:
- Customer explicitly asks to speak with a human
- Issue requires specialized technical knowledge
- Financial disputes or refunds over $100
- Customer is frustrated after 2 failed resolution attempts
- Security or fraud concerns
When transferring, explain why and reassure the customer.
## Transfer Destinations
- sip:support@company.com - General support, account questions
- sip:billing@company.com - Refunds, payments, billing issues
- sip:technical@company.com - Complex technical problems
## Navigating External Systems
If you need to check our partner system, use send_dtmf to navigate their IVR:
- Press 1 for account status
- Press 2 for order tracking
## Ending Calls
End the call when:
- Customer confirms their issue is resolved
- Customer says goodbye
- After successful transfer (the transfer tool handles this)
Before ending, always ask: "Is there anything else I can help you with?"
## Recording Call Outcomes
When using end_dialog, always capture:
- reason: Why the call ended (resolved, transferred, callback_requested, customer_ended)
- satisfaction: Customer's expressed satisfaction (1-5) if they mentioned it
- department: Which department resolved or will handle it
- resolution_notes: Brief summary of what was done1.3 Configure Voice Tools
Navigate to Call Configuration > Tools and configure each tool:

End Dialog Configuration
Enable End Dialog and configure the JSON Schema to capture structured data:
{
"type": "object",
"properties": {
"reason": {
"type": "string",
"enum": ["resolved", "transferred", "callback_requested", "customer_ended"],
"description": "Why the call ended"
},
"satisfaction": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"description": "Customer satisfaction rating if expressed"
},
"department": {
"type": "string",
"description": "Department that resolved or will handle the issue"
},
"resolution_notes": {
"type": "string",
"description": "Brief summary of what was resolved or the issue"
}
},
"required": ["reason"]
}Description:
End the call when the customer's issue is resolved or they wish to hang up.
Always ask if there's anything else before ending. Capture the call outcome
data including reason, satisfaction, and resolution notes.Transfer Call Configuration
Enable Transfer Call and add your destinations:
| Destination | Description |
|---|---|
sip:support@company.com | General support team |
sip:billing@company.com | Billing department |
sip:technical@company.com | Technical support |
Description:
Transfer to:
- support@company.com: General issues, account questions, product info
- billing@company.com: Refunds, payment disputes, billing errors, invoices
- technical@company.com: Complex technical issues, system outages, integrationsReceive DTMF Configuration
Enable Receive DTMF with:
- Default Timeout: 30 seconds
Description:
Collect customer identification number (SSN) or other numeric input.
Always confirm the digits before proceeding.Send DTMF Configuration
Enable Send DTMF with:
Description:
Navigate external IVR systems when checking partner services.
Send appropriate digits based on the menu options.1.4 Configure Greeting
Set Greeting Strategy to "AI Generated" with:
Greet the customer warmly, introduce yourself as the AI assistant for
Acme Corp, and begin the identification process by asking them to enter
their Social Security Number using the phone keypad.Part 2: PBX Integration via SIP Headers
2.1 How It Works
For SIP calls, the system creates threads automatically when a call connects to your agent. You don't need to call any API before the call starts. Instead, you pass context to the agent via SIP headers that your PBX adds to the SIP INVITE.
┌─────────────┐ ┌──────────────────────────┐
│ Your PBX │──── SIP INVITE ─────────────▶│ <agent_id>@sip.sippulse.ai │
│ │ Headers: │ │
│ call-id: │ x-uid: abc123 │ System creates thread │
│ abc123 │ x-additional- │ automatically with │
│ │ instructions: ... │ uid = "abc123" │
└─────────────┘ └──────────────────────────┘Integration flow:
- Enable SIP on your agent and copy the SIP address (e.g.,
agt_xxxx@sip.sippulse.ai) - Configure your PBX to route calls to that address
- Add SIP headers to pass context (UID, additional instructions)
- After the call ends, fetch the thread by UID to extract outcomes
Where to Get the SIP Address
The agent's SIP address is configured in the platform under Deploy > SIP. See the SIP Integration guide for details.
2.2 Supported SIP Headers
| Header | Required | Description |
|---|---|---|
x-uid | Recommended | Links the thread to your PBX call-id for later retrieval |
x-additional-instructions | Optional | Extra context for the agent (caller info, DID, customer data) |
x-vars Not Yet Supported
The x-vars header is not currently supported. Use x-additional-instructions to pass context to the agent as natural language instructions.
2.3 Configuring Your PBX
Asterisk (PJSIP)
; extensions.conf
[incoming]
exten => _X.,1,NoOp(Incoming call from ${CALLERID(num)})
same => n,Set(PJSIP_HEADER(add,x-uid)=${UNIQUEID})
same => n,Set(PJSIP_HEADER(add,x-additional-instructions)=Caller: ${CALLERID(num)}, DID: ${EXTEN})
same => n,Dial(PJSIP/sippulse-trunk/${EXTEN})FreeSWITCH
<!-- dialplan/default.xml -->
<extension name="route_to_sippulse">
<condition field="destination_number" expression="^(.*)$">
<action application="set" data="sip_h_x-uid=${uuid}"/>
<action application="set" data="sip_h_x-additional-instructions=Caller: ${caller_id_number}, DID: ${destination_number}"/>
<action application="bridge" data="sofia/external/agt_xxxx@sip.sippulse.ai"/>
</condition>
</extension>Generic SIP Trunk
Most SIP systems allow adding custom headers. The key headers to add:
x-uid: <your-pbx-call-id>
x-additional-instructions: Caller: +15551234567, DID: +18005551234UID vs Thread ID
- Thread ID (
thr_xxx): SipPulse's internal identifier - UID: Your external identifier (call-id, ticket number, etc.)
You can fetch threads using either. If the ID starts with thr_, it's treated as internal. Otherwise, it's looked up by UID.
2.4 Fetching Call Outcomes
After a call ends, retrieve the thread by UID to extract outcome data:
import json
def get_call_outcome(call_id: str) -> dict:
"""Fetch thread by PBX call-id and extract outcome data"""
response = requests.get(
f"{BASE_URL}/threads/{call_id}", # Use UID directly
headers={"api-key": API_KEY}
)
thread = response.json()
outcome = {
"end_dialog": None,
"transfer_destination": None,
"dtmf_collected": [],
}
# Search through message history for tool calls
for message in thread.get("history", []):
tool_calls = message.get("tool_calls", [])
for tool_call in tool_calls:
func = tool_call.get("function", {})
name = func.get("name")
args = json.loads(func.get("arguments", "{}"))
if name == "end_dialog":
outcome["end_dialog"] = args
elif name == "transfer_call":
outcome["transfer_destination"] = args.get("to")
elif name == "receive_dtmf":
# DTMF results are in tool response messages
pass
# Also check tool responses for DTMF data
for message in thread.get("history", []):
if message.get("role") == "tool":
content = message.get("content", "")
if "digits" in content:
try:
dtmf_data = json.loads(content)
outcome["dtmf_collected"].append(dtmf_data)
except:
pass
return outcome
# Usage example
outcome = get_call_outcome("pbx-call-abc123")
print(f"Call outcome: {outcome}")
# {
# 'end_dialog': {'reason': 'resolved', 'satisfaction': 4, 'department': 'support'},
# 'transfer_destination': None,
# 'dtmf_collected': [{'digits': '123456789', 'terminator': '#'}]
# }async function getCallOutcome(callId) {
const response = await fetch(
`${BASE_URL}/threads/${callId}`, // Use UID directly
{ headers: { "api-key": API_KEY } }
);
const thread = await response.json();
const outcome = {
endDialog: null,
transferDestination: null,
dtmfCollected: [],
};
// Search through message history for tool calls
for (const message of thread.history || []) {
for (const toolCall of message.tool_calls || []) {
const { name, arguments: args } = toolCall.function;
const parsedArgs = JSON.parse(args || "{}");
if (name === "end_dialog") {
outcome.endDialog = parsedArgs;
} else if (name === "transfer_call") {
outcome.transferDestination = parsedArgs.to;
}
}
// Check tool responses for DTMF data
if (message.role === "tool" && message.content?.includes("digits")) {
try {
const dtmfData = JSON.parse(message.content);
outcome.dtmfCollected.push(dtmfData);
} catch {}
}
}
return outcome;
}
// Usage
const outcome = await getCallOutcome("pbx-call-abc123");
console.log("Call outcome:", outcome);2.5 Checking Transfer Destination (Post-Call)
When the agent transfers a call, the system automatically performs a SIP REFER to route the call to the destination. This code is for querying where the call was transferred after it ends—useful for reporting and CRM updates:
def get_transfer_destination(call_id: str) -> str | None:
"""Check if call was transferred and return destination"""
response = requests.get(
f"{BASE_URL}/threads/{call_id}",
headers={"api-key": API_KEY}
)
thread = response.json()
for message in thread.get("history", []):
for tool_call in message.get("tool_calls", []):
if tool_call["function"]["name"] == "transfer_call":
args = json.loads(tool_call["function"]["arguments"])
return args.get("to")
return None
# Usage
destination = get_transfer_destination("pbx-call-abc123")
if destination:
print(f"Call was transferred to: {destination}")
else:
print("Call was not transferred")async function getTransferDestination(callId) {
const response = await fetch(
`${BASE_URL}/threads/${callId}`,
{ headers: { "api-key": API_KEY } }
);
const thread = await response.json();
for (const message of thread.history || []) {
for (const toolCall of message.tool_calls || []) {
if (toolCall.function.name === "transfer_call") {
const args = JSON.parse(toolCall.function.arguments);
return args.to;
}
}
}
return null;
}2.6 Complete Integration Example
Since threads are created automatically when SIP calls connect, you only need to handle the call-end event to extract outcomes. Here's an example webhook handler:
from flask import Flask, request, jsonify
import requests
import json
app = Flask(__name__)
API_KEY = "your_api_key"
BASE_URL = "https://api.sippulse.ai"
@app.route("/webhook/call-end", methods=["POST"])
def handle_call_end():
"""
Called when call ends - fetch thread by UID and update CRM.
The UID matches the x-uid header your PBX added to the SIP INVITE.
"""
data = request.json
call_id = data["call_id"] # This is the same value you set in x-uid header
# Fetch thread from SipPulse using the UID
response = requests.get(
f"{BASE_URL}/threads/{call_id}",
headers={"api-key": API_KEY}
)
thread = response.json()
# Extract outcome data from thread history
outcome = extract_outcome(thread)
# Update your CRM/ticketing system
if outcome["end_dialog"]:
update_crm_ticket(
call_id=call_id,
reason=outcome["end_dialog"].get("reason"),
satisfaction=outcome["end_dialog"].get("satisfaction"),
notes=outcome["end_dialog"].get("resolution_notes"),
)
if outcome["transfer_destination"]:
log_transfer(call_id, outcome["transfer_destination"])
return jsonify({"status": "processed"})
def extract_outcome(thread: dict) -> dict:
"""Extract end_dialog and transfer data from thread history"""
outcome = {"end_dialog": None, "transfer_destination": None}
for message in thread.get("history", []):
for tool_call in message.get("tool_calls", []):
func = tool_call.get("function", {})
name = func.get("name")
args = json.loads(func.get("arguments", "{}"))
if name == "end_dialog":
outcome["end_dialog"] = args
elif name == "transfer_call":
outcome["transfer_destination"] = args.get("to")
return outcome
def update_crm_ticket(call_id, reason, satisfaction, notes):
"""Update your CRM with call outcome"""
print(f"Updating CRM for {call_id}: {reason}, satisfaction={satisfaction}")
def log_transfer(call_id, destination):
"""Log transfer for reporting"""
print(f"Call {call_id} transferred to {destination}")
if __name__ == "__main__":
app.run(port=5000)const express = require("express");
const app = express();
app.use(express.json());
const API_KEY = "your_api_key";
const BASE_URL = "https://api.sippulse.ai";
/**
* Called when call ends - fetch thread by UID and update CRM.
* The UID matches the x-uid header your PBX added to the SIP INVITE.
*/
app.post("/webhook/call-end", async (req, res) => {
const { call_id } = req.body; // Same value you set in x-uid header
// Fetch thread from SipPulse using the UID
const response = await fetch(`${BASE_URL}/threads/${call_id}`, {
headers: { "api-key": API_KEY }
});
const thread = await response.json();
// Extract outcome data from thread history
const outcome = extractOutcome(thread);
// Update your CRM/ticketing system
if (outcome.endDialog) {
await updateCrmTicket(
call_id,
outcome.endDialog.reason,
outcome.endDialog.satisfaction,
outcome.endDialog.resolution_notes
);
}
if (outcome.transferDestination) {
await logTransfer(call_id, outcome.transferDestination);
}
res.json({ status: "processed" });
});
function extractOutcome(thread) {
const outcome = { endDialog: null, transferDestination: null };
for (const message of thread.history || []) {
for (const toolCall of message.tool_calls || []) {
const { name, arguments: args } = toolCall.function;
const parsedArgs = JSON.parse(args || "{}");
if (name === "end_dialog") {
outcome.endDialog = parsedArgs;
} else if (name === "transfer_call") {
outcome.transferDestination = parsedArgs.to;
}
}
}
return outcome;
}
async function updateCrmTicket(callId, reason, satisfaction, notes) {
console.log(`Updating CRM for ${callId}: ${reason}, satisfaction=${satisfaction}`);
}
async function logTransfer(callId, destination) {
console.log(`Call ${callId} transferred to ${destination}`);
}
app.listen(5000, () => console.log("Webhook server running on port 5000"));Part 3: Example Conversations
Resolved Call with DTMF Identification
Agent: "Hello! Welcome to Acme Corp customer service. I'm your AI assistant. To help you, please enter your 9-digit Social Security Number using your phone keypad. Press pound when you're done, or star to start over if you make a mistake."
Customer enters: 1-2-3-4-5-6-7-8-9-#
Agent: "Thank you! I received: one, two, three, four, five, six, seven, eight, nine. Is that correct?"
Customer: "Yes, that's right."
Agent: "Perfect, I found your account, John. How can I help you today?"
Customer: "I need to check my order status."
Agent: "I can help with that. Let me look up your recent orders... I see order #12345 shipped yesterday and should arrive by Friday. Would you like the tracking number?"
Customer: "No, that's fine. Thanks!"
Agent: "You're welcome! Is there anything else I can help you with?"
Customer: "No, that's all."
Agent: "Great! Thank you for calling Acme Corp. Have a wonderful day!"
Agent ends call with:
{reason: "resolved", satisfaction: null, department: "support"}
Transfer to Human Agent
Customer: "I was charged twice for my order and I want a refund!"
Agent: "I apologize for that inconvenience. I can see this is a billing issue that our billing team can resolve directly for you. Let me transfer you to them right now—they'll be able to process your refund. Please hold for just a moment."
Agent transfers to
sip:billing@company.com
Error Handling in DTMF Collection
Agent: "...I received: one, two, three, four. Is that correct?"
Customer: "No, I made a mistake."
Agent: "No problem! Let's try again. Please enter your 9-digit Social Security Number. Remember, press star to clear and start over, pound when you're done."
Customer presses star, then enters correct number
Part 4: How It Works Internally
Transfer Call Flow
When the agent decides to transfer:
- LLM generates tool call with the destination
- Voice system receives the transfer request
- Agent finishes speaking ("Let me transfer you...")
- SIP REFER is sent to your SIP trunk
- Call is transferred to the destination
Agent speaks → Wait for TTS complete → SIP REFER → Call transferredDTMF Collection Flow
The DTMF collection has sophisticated handling:
- Type-ahead buffering: Customer can start typing before agent finishes speaking
- Smart termination: Ends on expected digit count OR terminator key (#)
- Reset capability: Star (*) clears buffer to start over
- Timeout handling: Graceful fallback if no input received
Agent: "Enter your SSN..."
│
├── Customer types immediately (buffered)
│
└── Agent finishes speaking
│
└── Collection starts/continues
│
├── # pressed → Return digits
├── 9 digits → Return digits
├── * pressed → Clear, wait for input
└── Timeout → Return partial or emptyPart 5: Troubleshooting
DTMF Not Being Collected
| Symptom | Likely Cause | Solution |
|---|---|---|
| Tool never triggers | Tool not enabled | Check Call Configuration > Tools |
| No digits received | SIP codec issue | Verify DTMF method (RFC 2833 or SIP INFO) |
| Partial digits | Network latency | Increase timeout, check connection |
| Digits not confirmed | Prompt issue | Update agent instructions to always confirm |
Transfer Not Working
| Symptom | Likely Cause | Solution |
|---|---|---|
| Transfer fails | Invalid SIP URI | Verify format: sip:user@domain |
| Wrong destination | Unclear instructions | Be more specific in tool description |
| Transfer loops | Agent confused | Add clearer conditions in prompt |
| SIP error | Trunk doesn't support REFER | Check with your SIP provider |
End Dialog Issues
| Symptom | Likely Cause | Solution |
|---|---|---|
| Never ends | Tool not enabled | Enable End Dialog in tools |
| Ends too early | Conditions too broad | Refine description and prompt |
| Missing data | Schema mismatch | Verify JSON Schema is valid |
| Partial data | Agent not filling all fields | Make required fields explicit in prompt |
UID Lookup Fails
| Symptom | Likely Cause | Solution |
|---|---|---|
| Thread not found | UID mismatch | Verify exact UID match |
| Wrong thread | Duplicate UIDs | Ensure UIDs are unique per organization |
| Empty history | Thread not used | Verify thread was actually used in call |
Related Documentation
- Voice Call Tools Reference - Complete tool specifications
- SIP Integration - SIP deployment configuration
- Agent Configuration - Full configuration reference
- REST API Guide - API integration details
- Prompting Agents - Writing effective instructions
