Skip to content

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:

  1. Greets callers and asks for identification
  2. Collects SSN via phone keypad (receive_dtmf) for customer lookup
  3. Resolves issues or...
  4. Transfers to human agents (transfer_call) when needed, or...
  5. Navigates external IVRs (send_dtmf) to check other systems
  6. 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_dialog data 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:

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

  1. Navigate to Agents and click Create Agent
  2. Select Voice Agent
  3. Name it: "Customer Service Agent"
  4. 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:

markdown
# 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 done

1.3 Configure Voice Tools

Navigate to Call Configuration > Tools and configure each tool:

Voice Tools Configuration

End Dialog Configuration

Enable End Dialog and configure the JSON Schema to capture structured data:

json
{
  "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:

DestinationDescription
sip:support@company.comGeneral support team
sip:billing@company.comBilling department
sip:technical@company.comTechnical 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, integrations

Receive 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:

  1. Enable SIP on your agent and copy the SIP address (e.g., agt_xxxx@sip.sippulse.ai)
  2. Configure your PBX to route calls to that address
  3. Add SIP headers to pass context (UID, additional instructions)
  4. 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

HeaderRequiredDescription
x-uidRecommendedLinks the thread to your PBX call-id for later retrieval
x-additional-instructionsOptionalExtra 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)

ini
; 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

xml
<!-- 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: +18005551234

UID 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:

python
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': '#'}]
# }
javascript
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:

python
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")
javascript
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:

python
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)
javascript
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:

  1. LLM generates tool call with the destination
  2. Voice system receives the transfer request
  3. Agent finishes speaking ("Let me transfer you...")
  4. SIP REFER is sent to your SIP trunk
  5. Call is transferred to the destination
Agent speaks → Wait for TTS complete → SIP REFER → Call transferred

DTMF Collection Flow

The DTMF collection has sophisticated handling:

  1. Type-ahead buffering: Customer can start typing before agent finishes speaking
  2. Smart termination: Ends on expected digit count OR terminator key (#)
  3. Reset capability: Star (*) clears buffer to start over
  4. 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 empty

Part 5: Troubleshooting

DTMF Not Being Collected

SymptomLikely CauseSolution
Tool never triggersTool not enabledCheck Call Configuration > Tools
No digits receivedSIP codec issueVerify DTMF method (RFC 2833 or SIP INFO)
Partial digitsNetwork latencyIncrease timeout, check connection
Digits not confirmedPrompt issueUpdate agent instructions to always confirm

Transfer Not Working

SymptomLikely CauseSolution
Transfer failsInvalid SIP URIVerify format: sip:user@domain
Wrong destinationUnclear instructionsBe more specific in tool description
Transfer loopsAgent confusedAdd clearer conditions in prompt
SIP errorTrunk doesn't support REFERCheck with your SIP provider

End Dialog Issues

SymptomLikely CauseSolution
Never endsTool not enabledEnable End Dialog in tools
Ends too earlyConditions too broadRefine description and prompt
Missing dataSchema mismatchVerify JSON Schema is valid
Partial dataAgent not filling all fieldsMake required fields explicit in prompt

UID Lookup Fails

SymptomLikely CauseSolution
Thread not foundUID mismatchVerify exact UID match
Wrong threadDuplicate UIDsEnsure UIDs are unique per organization
Empty historyThread not usedVerify thread was actually used in call