Duration 2 hours
Day 4 of 5

Learning Objectives

By the end of this module, students will be able to:

  • Configure call recording settings
  • Control recording programmatically
  • Handle sensitive data appropriately
  • Implement compliance requirements

Topics

1. Recording Basics (20 min)

Enabling Recording

from signalwire_agents import AgentBase

agent = AgentBase(name="recorded-agent")

# Enable recording
agent.set_params({
    "record_call": True,
    "record_format": "mp3"
})

Recording Formats

Format Quality Size Use Case
mp3 Good Small Archival
wav Best Large Analysis

Stereo vs Mono

agent.set_params({
    "record_call": True,
    "record_stereo": True  # Separate channels for caller/agent
})

Stereo: Left = caller, Right = agent (better for transcription) Mono: Mixed audio (smaller files)


2. Recording Controls (30 min)

Start Recording

@agent.tool(description="Start recording the call")
def start_recording(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    return (
        SwaigFunctionResult("I'm now recording this call for quality purposes.")
        .record_call(control_id="main")
    )

Stop Recording

@agent.tool(description="Stop recording")
def stop_recording(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    return (
        SwaigFunctionResult("Recording stopped.")
        .stop_record_call(control_id="main")
    )

Pause/Resume

@agent.tool(description="Pause recording for sensitive info")
def pause_recording(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    return (
        SwaigFunctionResult("Recording paused. Please provide your information.")
        .stop_record_call(control_id="main")
    )

@agent.tool(description="Resume recording")
def resume_recording(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    return (
        SwaigFunctionResult("Recording resumed.")
        .record_call(control_id="main")
    )

3. Sensitive Data Handling (25 min)

Auto-Pause Pattern

For general sensitive data (SSN, DOB, etc.), use the secure=True flag:

@agent.tool(
    description="Collect sensitive verification info",
    secure=True  # Recording pauses automatically
)
def collect_sensitive_info(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    ssn_last_four = args.get("ssn_last_four", "")
    # Process sensitive data
    return SwaigFunctionResult(f"Verified SSN ending in {ssn_last_four}.")

Secure Payment Collection with pay()

For credit card collection, use the SWML pay method instead of SWAIG functions. This keeps card data away from the LLM entirely - card data is collected via IVR and sent directly to your payment gateway:

@agent.tool(
    description="Process payment for customer",
    parameters={
        "type": "object",
        "properties": {
            "amount": {"type": "string", "description": "Amount to charge"}
        },
        "required": ["amount"]
    }
)
def process_payment(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    amount = args.get("amount", "0.00")

    # Get public URL from SDK (auto-detected from ngrok/proxy headers)
    base_url = self.get_full_url().rstrip('/')
    payment_url = f"{base_url}/payment"

    return (
        SwaigFunctionResult(
            "I'll collect your payment securely. "
            "Please enter your card using your phone keypad.",
            post_process=True
        )
        .pay(
            payment_connector_url=payment_url,
            charge_amount=amount,
            input_method="dtmf",
            security_code=True,
            postal_code=True
        )
    )

Benefits of the pay method:

  • Card data never passes through the LLM
  • Card data collected via IVR (DTMF or speech)
  • Data sent directly to your payment gateway
  • Only pay_result (success/failure) returned to agent

Manual Pause for Sensitive Sections

@agent.tool(description="Collect social security number")
def collect_ssn(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    return (
        SwaigFunctionResult(
            "For verification, I need your social security number. "
            "Recording is paused for your privacy."
        )
        .stop_record_call(control_id="main")
    )

@agent.tool(description="SSN received")
def ssn_received(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    ssn = args.get("ssn", "")
    # Store securely, not in logs
    last_four = ssn[-4:]

    return (
        SwaigFunctionResult(f"SSN ending in {last_four} verified. Resuming recording.")
        .record_call(control_id="main")
        .update_global_data( {"ssn_verified": True})
    )

4. Compliance Patterns (25 min)

Recording Disclosure

Many jurisdictions require disclosure:

class CompliantAgent(AgentBase):
    def __init__(self):
        super().__init__(name="compliant-agent")

        self.prompt_add_section(
            "Recording Disclosure",
            "At the start of every call, inform the caller: "
            "'This call may be recorded for quality and training purposes.' "
            "Wait for acknowledgment before proceeding."
        )

        # Start with recording off
        self.set_params({
            "record_call": False
        })

    @AgentBase.tool(description="Get consent and start recording")
    def get_recording_consent(self, args: dict, raw_data: dict = None) -> SwaigFunctionResult:
        consent = args.get("consent", "")
        if consent.lower() in ["yes", "okay", "sure", "fine"]:
            return (
                SwaigFunctionResult("Thank you. How can I help you today?")
                .record_call(control_id="main")
                .update_global_data( {"recording_consent": True})
            )
        else:
            return SwaigFunctionResult(
                "No problem. This call will not be recorded. "
                "How can I help you today?"
            )

Recording with Beep

agent.set_params({
    "record_call": True,
    "record_beep": True  # Periodic beep to indicate recording
})

Retention Notices

@agent.tool(description="Inform about recording retention")
def recording_info(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    return SwaigFunctionResult(
        "Call recordings are retained for 90 days for quality purposes, "
        "then automatically deleted. You can request a copy or deletion "
        "by emailing privacy@example.com."
    )

5. Recording Webhooks (20 min)

Receiving Recording Status

Configure a webhook to receive recording events:

agent.set_params({
    "record_call": True,
    "record_webhook_url": "https://your-server.com/recording-status"
})

Your webhook receives:

{
  "call_id": "abc123",
  "recording_url": "https://signalwire.com/recordings/xyz.mp3",
  "duration": 180,
  "status": "completed"
}

Processing Recordings

from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/recording-status")
async def handle_recording(request: Request):
    data = await request.json()

    if data["status"] == "completed":
        # Store recording metadata
        save_recording_metadata({
            "call_id": data["call_id"],
            "url": data["recording_url"],
            "duration": data["duration"]
        })

        # Optionally trigger transcription
        queue_transcription(data["recording_url"])

    return {"status": "received"}

Complete Example

Click to reveal complete solution
#!/usr/bin/env python3
"""Compliant call recording agent."""

from signalwire_agents import AgentBase, SwaigFunctionResult


class RecordingAgent(AgentBase):
    def __init__(self):
        super().__init__(name="recording-agent")

        self.prompt_add_section(
            "Role",
            "You help customers with account inquiries. "
            "Always disclose recording at call start."
        )

        self.prompt_add_section(
            "Recording Policy",
            bullets=[
                "Disclose recording at the start of the call",
                "Pause recording when collecting sensitive data",
                "Resume recording after sensitive data is processed",
                "Never read back sensitive information"
            ]
        )

        self.add_language("English", "en-US", "rime.spore")

        # Recording configuration
        self.set_params({
            "record_call": True,
            "record_format": "mp3",
            "record_stereo": True
        })

        self._setup_functions()

    def _setup_functions(self):
        @self.tool(description="Pause recording for sensitive info")
        def secure_mode(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            return (
                SwaigFunctionResult(
                    "Recording paused. Please go ahead with your sensitive information."
                )
                .stop_record_call(control_id="main")
            )

        @self.tool(description="Resume normal recording")
        def normal_mode(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            return (
                SwaigFunctionResult("Thank you. Recording resumed.")
                .record_call(control_id="main")
            )

        @self.tool(
            description="Process payment for customer",
            parameters={
                "type": "object",
                "properties": {
                    "amount": {"type": "string", "description": "Amount to charge"}
                },
                "required": ["amount"]
            }
        )
        def process_payment(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
            amount = args.get("amount", "0.00")

            # Get public URL from SDK (auto-detected from ngrok/proxy headers)
            base_url = self.get_full_url().rstrip('/')
            payment_url = f"{base_url}/payment"

            # Card data collected via IVR, never touches the LLM
            return (
                SwaigFunctionResult(
                    "I'll collect your payment securely. "
                    "Please enter your card using your phone keypad.",
                    post_process=True
                )
                .pay(
                    payment_connector_url=payment_url,
                    charge_amount=amount,
                    input_method="dtmf",
                    security_code=True,
                    postal_code=True,
                    ai_response=(
                        "The payment result is ${pay_result}. "
                        "If successful, confirm the payment. "
                        "If failed, offer to try another card."
                    )
                )
            )


if __name__ == "__main__":
    agent = RecordingAgent()
    agent.run()

Key Takeaways

  1. Recording is configurable - Format, stereo, beep
  2. Control via actions - Start, stop, pause, resume
  3. Protect sensitive data - Pause for PCI/PII
  4. Compliance matters - Disclosure, consent, retention
  5. Webhooks for processing - Receive completed recordings

Preparation for Lab 2.7

  • Review your organization’s recording policies
  • Identify sensitive data your agent handles
  • Plan pause/resume points

Lab Preview

In Lab 2.7, you will:

  1. Configure call recording
  2. Implement consent collection
  3. Add pause/resume for sensitive data
  4. Test recording controls

Back to top

SignalWire AI Agents Certification Program