| name | twilio-testing |
| description | Test and validate Twilio Voice API integration with ElevenLabs ConversationRelay for outbound calling. Use when setting up Twilio integration, testing outbound calls, validating audio quality, configuring webhooks, measuring call metrics, or debugging telephony issues. Includes ConversationRelay setup, call execution, and quality validation. |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep, WebFetch |
Twilio Testing Skill
Comprehensive testing framework for Twilio Voice API + ElevenLabs ConversationRelay integration on the Next Level Real Estate platform. This skill provides systematic testing procedures, quality validation, and troubleshooting guidance.
When to Use This Skill
Invoke this skill when you need to:
- ✅ Set up Twilio Voice API integration
- ✅ Configure ElevenLabs ConversationRelay
- ✅ Execute test outbound calls
- ✅ Validate call audio quality
- ✅ Measure call performance metrics
- ✅ Debug telephony connection issues
- ✅ Test TCPA compliance checkpoints
- ✅ Verify webhook endpoints
Twilio + ElevenLabs Architecture
┌─────────────────┐
│ Next Level RE │
│ Application │
└────────┬────────┘
│
│ 1. Initiate call with
│ lead context
▼
┌─────────────────┐
│ Twilio Voice │◄──── Phone Number
│ API │
└────────┬────────┘
│
│ 2. ConversationRelay
│ webhook
▼
┌─────────────────┐
│ ElevenLabs │
│ Conversational │
│ AI │
└────────┬────────┘
│
│ 3. Bidirectional
│ audio stream
▼
┌─────────────────┐
│ Lead's Phone │
└─────────────────┘
Setup Checklist
Prerequisites
- [ ] Active Twilio account
- [ ] Account SID
- [ ] Auth Token
- [ ] At least one phone number (verified or purchased)
- [ ] Voice API enabled
- [ ] ElevenLabs API key
- [ ] At least one conversational agent created
- [ ] Agent ID noted
- [ ] TWILIO_ACCOUNT_SID set
- [ ] TWILIO_AUTH_TOKEN set
- [ ] TWILIO_PHONE_NUMBER set
- [ ] ELEVENLABS_API_KEY set
- [ ] Webhook endpoint accessible (public URL)
Environment Setup
Create .env file with required credentials:
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_PHONE_NUMBER=+1234567890
ELEVENLABS_API_KEY=your_elevenlabs_api_key
ELEVENLABS_AGENT_ID=agent_xxxxxxxx
WEBHOOK_BASE_URL=https://your-domain.com
WEBHOOK_SECRET=your_webhook_secret
TEST_PHONE_NUMBER=+1234567890
Integration Setup
Step 1: Install Twilio SDK
cd services/calling-service
npm install twilio
Step 2: Create Twilio Client Service
Create services/calling-service/src/clients/twilio.ts:
import twilio from 'twilio'
export interface TwilioConfig {
accountSid: string
authToken: string
phoneNumber: string
}
export interface CallConfig {
to: string
conversationId: string
webhookUrl: string
}
export function createTwilioClient(config: TwilioConfig) {
const client = twilio(config.accountSid, config.authToken)
return {
async initiateCall(callConfig: CallConfig) {
const call = await client.calls.create({
from: config.phoneNumber,
to: callConfig.to,
url: `${callConfig.webhookUrl}/twiml/${callConfig.conversationId}`,
statusCallback: `${callConfig.webhookUrl}/status/${callConfig.conversationId}`,
statusCallbackEvent: ['initiated', 'ringing', 'answered', 'completed'],
record: true,
})
return {
callSid: call.sid,
status: call.status,
to: call.to,
from: call.from,
}
},
async getCallStatus(callSid: string) {
const call = await client.calls(callSid).fetch()
return {
sid: call.sid,
status: call.status,
duration: call.duration,
startTime: call.startTime,
endTime: call.endTime,
}
},
async hangupCall(callSid: string) {
await client.calls(callSid).update({ status: 'completed' })
},
}
}
Step 3: Create ConversationRelay Webhook Handler
Create services/calling-service/src/routes/twiml.ts:
import { Router } from 'express'
import twilio from 'twilio'
const router = Router()
const VoiceResponse = twilio.twiml.VoiceResponse
router.post('/twiml/:conversationId', async (req, res) => {
const { conversationId } = req.params
const conversation = await getElevenLabsConversation(conversationId)
const twiml = new VoiceResponse()
if (conversation.recordingConsent) {
twiml.say({
voice: 'alice',
language: 'en-US'
}, 'This call may be recorded for quality and training purposes.')
}
const connect = twiml.connect()
connect.stream({
url: `wss://api.elevenlabs.io/v1/convai/${conversationId}/stream`,
parameters: {
apiKey: process.env.ELEVENLABS_API_KEY,
agentId: conversation.agentId,
}
})
res.type('text/xml')
res.send(twiml.toString())
})
router.post('/status/:conversationId', async (req, res) => {
const { conversationId } = req.params
const { CallStatus, CallDuration, RecordingUrl } = req.body
console.log(`Call ${conversationId}: ${CallStatus}`)
await updateConversationStatus(conversationId, {
status: CallStatus,
duration: CallDuration,
recordingUrl: RecordingUrl,
})
res.sendStatus(200)
})
export default router
Step 4: Test Webhook Endpoint
npm install -g ngrok
npm run dev
ngrok http 3000
Testing Framework
Test Categories
- Integration Tests - Verify component connectivity
- Functional Tests - Validate end-to-end call flow
- Quality Tests - Measure audio and conversation quality
- Performance Tests - Track metrics under load
- Compliance Tests - Verify TCPA requirements
Test 1: Webhook Connectivity
Objective: Verify Twilio can reach your webhook endpoint
curl -X POST https://your-domain.com/twiml/test-123 \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "From=+1234567890&To=+0987654321"
Success Criteria:
Test 2: ElevenLabs Agent Health
Objective: Verify agent is configured and accessible
Use mcp__elevenlabs__elevenlabs_get_agent with:
{
"agentId": "your_agent_id"
}
Success Criteria:
Test 3: Test Outbound Call
Objective: Execute complete call flow end-to-end
async function testOutboundCall() {
const conversation = await startElevenLabsConversation({
agentId: process.env.ELEVENLABS_AGENT_ID,
leadData: {
name: "Test Lead",
phone: process.env.TEST_PHONE_NUMBER,
propertyAddress: "123 Test St"
},
maxDuration: 120
})
const call = await twilioClient.initiateCall({
to: process.env.TEST_PHONE_NUMBER,
conversationId: conversation.conversationId,
webhookUrl: process.env.WEBHOOK_BASE_URL
})
console.log(`Test call initiated:`)
console.log(`- Call SID: ${call.callSid}`)
console.log(`- Conversation ID: ${conversation.conversationId}`)
console.log(`- Status: ${call.status}`)
let callStatus = call.status
while (callStatus !== 'completed' && callStatus !== 'failed') {
await sleep(5000)
const status = await twilioClient.getCallStatus(call.callSid)
callStatus = status.status
console.log(`Call status: ${callStatus}`)
}
const conversationDetails = await getElevenLabsConversation(
conversation.conversationId
)
console.log(`\nTest Results:`)
console.log(`- Call Status: ${callStatus}`)
console.log(`- Duration: ${conversationDetails.duration}s`)
console.log(`- Sentiment: ${conversationDetails.sentiment?.overall}`)
console.log(`- Transcript lines: ${conversationDetails.transcript?.length}`)
return {
success: callStatus === 'completed',
callSid: call.callSid,
conversationId: conversation.conversationId,
duration: conversationDetails.duration,
transcript: conversationDetails.transcript
}
}
Success Criteria:
Test 4: Audio Quality Validation
Objective: Measure and validate call audio quality
Manual Checklist:
## Audio Quality Checklist
### Agent Audio (What You Hear)
- [ ] Voice is clear and intelligible
- [ ] No robotic or choppy artifacts
- [ ] Volume is appropriate (not too loud/quiet)
- [ ] No echo or feedback
- [ ] Natural pacing and rhythm
### User Audio (What Agent Hears)
- [ ] Agent responds to your speech accurately
- [ ] No repeated requests for clarification
- [ ] Agent picks up on tone/sentiment
- [ ] Background noise doesn't confuse agent
### Technical Metrics
- [ ] Response latency <200ms
- [ ] No audio dropouts >1 second
- [ ] Turn-taking feels natural
- [ ] No awkward pauses >3 seconds
Automated Metrics:
interface AudioQualityMetrics {
avgResponseLatency: number
maxResponseLatency: number
audioDropouts: number
turnTakingQuality: number
clarificationRequests: number
}
async function measureAudioQuality(conversationId: string) {
const conversation = await getElevenLabsConversation(conversationId)
const metrics: AudioQualityMetrics = {
avgResponseLatency: calculateAvgLatency(conversation.transcript),
maxResponseLatency: calculateMaxLatency(conversation.transcript),
audioDropouts: countDropouts(conversation.transcript),
turnTakingQuality: scoreTurnTaking(conversation.transcript),
clarificationRequests: countClarifications(conversation.transcript)
}
const quality = {
excellent: metrics.avgResponseLatency < 150 &&
metrics.audioDropouts === 0 &&
metrics.turnTakingQuality > 0.8,
good: metrics.avgResponseLatency < 250 &&
metrics.audioDropouts < 2 &&
metrics.turnTakingQuality > 0.6,
acceptable: metrics.avgResponseLatency < 400 &&
metrics.audioDropouts < 5 &&
metrics.turnTakingQuality > 0.4
}
return { metrics, quality }
}
Test 5: Context Injection
Objective: Verify lead data is properly used in conversation
async function testContextInjection() {
const testLeadData = {
name: "John Smith",
phone: "+1234567890",
propertyAddress: "456 Oak Avenue, Austin TX",
estimatedValue: 250000,
motivation: "probate",
timeline: "urgent"
}
const conversation = await startElevenLabsConversation({
agentId: process.env.ELEVENLABS_AGENT_ID,
leadData: testLeadData,
propertyInfo: {
address: testLeadData.propertyAddress,
estimatedValue: testLeadData.estimatedValue,
condition: "needs_repairs"
}
})
const call = await twilioClient.initiateCall({
to: testLeadData.phone,
conversationId: conversation.conversationId,
webhookUrl: process.env.WEBHOOK_BASE_URL
})
const details = await getElevenLabsConversation(conversation.conversationId)
const transcript = details.transcript.map(t => t.text).join(' ')
const contextChecks = {
usedName: transcript.includes(testLeadData.name),
mentionedAddress: transcript.includes('Oak Avenue'),
referencedMotivation: transcript.toLowerCase().includes('probate'),
acknowledgedTimeline: transcript.toLowerCase().includes('urgent') ||
transcript.toLowerCase().includes('soon')
}
console.log('Context Injection Results:')
console.log(`- Used name: ${contextChecks.usedName ? '✓' : '✗'}`)
console.log(`- Mentioned address: ${contextChecks.mentionedAddress ? '✓' : '✗'}`)
console.log(`- Referenced motivation: ${contextChecks.referencedMotivation ? '✓' : '✗'}`)
console.log(`- Acknowledged timeline: ${contextChecks.acknowledgedTimeline ? '✓' : '✗'}`)
return contextChecks
}
Success Criteria:
Test 6: TCPA Compliance
Objective: Verify all TCPA 2025 requirements are met
## TCPA Compliance Checklist
### Pre-Call Requirements
- [ ] Written consent verified before call
- [ ] Lead not on national DNC registry
- [ ] Consent date within validity period
- [ ] Consent source documented
### During Call
- [ ] Recording disclosure at call start (if recording)
- [ ] Agent identifies company name
- [ ] Agent states purpose of call clearly
- [ ] Opt-out option provided if requested
### Post-Call
- [ ] Call details logged (date, time, duration, outcome)
- [ ] Recording URL captured
- [ ] Opt-out requests honored immediately
- [ ] Audit trail maintained
async function testTCPACompliance(conversationId: string) {
const conversation = await getElevenLabsConversation(conversationId)
const transcript = conversation.transcript.map(t => t.text).join(' ').toLowerCase()
const compliance = {
recordingDisclosure: transcript.includes('recorded') ||
transcript.includes('recording'),
companyIdentified: transcript.includes('next level real estate'),
purposeStated: transcript.includes('property') ||
transcript.includes('selling'),
optOutOffered: true
}
const allCompliant = Object.values(compliance).every(v => v === true)
return {
compliant: allCompliant,
checks: compliance,
issues: Object.entries(compliance)
.filter(([k, v]) => !v)
.map(([k]) => k)
}
}
Test 7: Load Testing
Objective: Verify system handles concurrent calls
async function loadTest(concurrentCalls: number = 10) {
console.log(`Starting load test with ${concurrentCalls} concurrent calls...`)
const testPromises = []
const results = []
for (let i = 0; i < concurrentCalls; i++) {
const promise = testOutboundCall()
.then(result => {
results.push({ success: true, ...result })
})
.catch(error => {
results.push({ success: false, error: error.message })
})
testPromises.push(promise)
await sleep(1000)
}
await Promise.all(testPromises)
const successCount = results.filter(r => r.success).length
const failureCount = results.filter(r => !r.success).length
const successRate = (successCount / concurrentCalls) * 100
console.log(`\nLoad Test Results:`)
console.log(`- Total calls: ${concurrentCalls}`)
console.log(`- Successful: ${successCount}`)
console.log(`- Failed: ${failureCount}`)
console.log(`- Success rate: ${successRate.toFixed(1)}%`)
if (failureCount > 0) {
console.log(`\nFailures:`)
results.filter(r => !r.success).forEach((r, i) => {
console.log(` ${i + 1}. ${r.error}`)
})
}
return {
totalCalls: concurrentCalls,
successCount,
failureCount,
successRate,
results
}
}
Success Criteria:
Metrics Collection
Call Metrics Dashboard
interface CallMetrics {
initiatedCalls: number
connectedCalls: number
failedCalls: number
connectRate: number
avgDuration: number
minDuration: number
maxDuration: number
avgAudioQuality: number
avgResponseLatency: number
audioDropoutRate: number
conversationCompletionRate: number
positiveSentimentRate: number
qualifiedLeadRate: number
}
async function collectMetrics(timeRange: { start: Date, end: Date }) {
const conversations = await listElevenLabsConversations({
startDate: timeRange.start,
endDate: timeRange.end
})
const metrics: CallMetrics = {
initiatedCalls: conversations.length,
connectedCalls: conversations.filter(c => c.status === 'completed').length,
failedCalls: conversations.filter(c => c.status === 'failed').length,
connectRate: 0,
avgDuration: average(conversations.map(c => c.duration || 0)),
minDuration: Math.min(...conversations.map(c => c.duration || Infinity)),
maxDuration: Math.max(...conversations.map(c => c.duration || 0)),
avgAudioQuality: average(conversations.map(c => c.audioQuality || 0)),
avgResponseLatency: average(conversations.map(c => c.avgLatency || 0)),
audioDropoutRate: (conversations.filter(c => c.audioDropouts > 0).length / conversations.length) * 100,
conversationCompletionRate: (conversations.filter(c => c.goalReached).length / conversations.length) * 100,
positiveSentimentRate: (conversations.filter(c => c.sentiment?.overall === 'positive').length / conversations.length) * 100,
qualifiedLeadRate: (conversations.filter(c => c.leadQualified).length / conversations.length) * 100
}
metrics.connectRate = (metrics.connectedCalls / metrics.initiatedCalls) * 100
return metrics
}
Troubleshooting Guide
Issue: Call Doesn't Connect
Symptoms:
- Call status stays "initiated" or "ringing"
- No audio heard
- Call fails immediately
Diagnosis Steps:
- Check Twilio phone number is verified/purchased
- Verify test number can receive calls
- Check webhook endpoint is accessible
- Review Twilio debugger logs
Solutions:
curl https://your-domain.com/twiml/test
curl -X GET "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/IncomingPhoneNumbers.json" \
-u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN"
Issue: Poor Audio Quality
Symptoms:
- Choppy or robotic voice
- Echo or feedback
- Audio dropouts
Diagnosis Steps:
- Check network latency
- Verify bandwidth availability
- Test with different voice model
- Check agent configuration
Solutions:
await updateAgent(agentId, {
modelId: "eleven_flash_v2_5",
responseLatency: 75
})
maxConcurrentCalls = 5
{
codec: 'opus',
jitterBufferSize: 'small'
}
Issue: Agent Doesn't Use Context
Symptoms:
- Generic responses
- Doesn't mention lead name
- Ignores property details
Diagnosis Steps:
- Verify context injection format
- Check agent system prompt references context
- Review transcript for context usage
Solutions:
const context = {
leadData: {
name: "John",
property: "123 Main St"
}
}
systemPrompt += `\n\nIMPORTANT: You will receive lead context including name and property address. Use these naturally in conversation.`
console.log('Context sent:', JSON.stringify(context, null, 2))
Issue: TCPA Compliance Failure
Symptoms:
- Missing recording disclosure
- No company identification
- Opt-out not honored
Diagnosis Steps:
- Review call transcript
- Check agent configuration
- Verify compliance flags enabled
Solutions:
const agent = await createAgent({
tcpaCompliance: true,
recordingConsent: true,
greeting: "Hi [name], this is [agent name] calling from Next Level Real Estate. This call may be recorded. [continue...]"
})
systemPrompt += `\n\nIF the person asks to be removed from the call list, apologize politely, confirm their request, and end the call immediately.`
Best Practices
1. Test in Stages
- Start with webhook connectivity
- Then test single call
- Scale to multiple calls
- Finally, load test
2. Use Test Numbers
- Don't test on real leads initially
- Use your own phone for testing
- Keep a list of test numbers
- Document test scenarios
3. Monitor Continuously
- Set up alerting for failed calls
- Track metrics daily
- Review transcripts weekly
- Update agents based on learnings
4. Document Everything
- Keep test logs
- Record test scenarios
- Document failures and fixes
- Maintain troubleshooting runbook
5. Compliance First
- Always verify consent
- Check DNC before every call
- Include recording disclosure
- Honor opt-outs immediately
Testing Checklist
## Pre-Production Testing Checklist
### Infrastructure
- [ ] Twilio account configured
- [ ] Phone numbers purchased/verified
- [ ] Webhook endpoints accessible
- [ ] ElevenLabs agents created
- [ ] Environment variables set
### Integration
- [ ] Webhook connectivity tested
- [ ] TwiML response validated
- [ ] ConversationRelay configured
- [ ] Status callbacks working
### Functionality
- [ ] Single call successful
- [ ] Audio quality acceptable
- [ ] Context injection working
- [ ] Agent responds appropriately
- [ ] Call completes gracefully
### Quality
- [ ] Response latency <200ms
- [ ] No audio dropouts
- [ ] Natural turn-taking
- [ ] Clear voice quality
### Compliance
- [ ] Recording disclosure present
- [ ] Company identified
- [ ] Purpose stated
- [ ] Opt-out option available
- [ ] Consent verified
### Performance
- [ ] Load test passed (10+ concurrent)
- [ ] Success rate >95%
- [ ] No system crashes
- [ ] Metrics collected
### Documentation
- [ ] Test results documented
- [ ] Issues logged
- [ ] Troubleshooting guide updated
- [ ] Team trained
Resources
Internal Documentation
- Twilio client:
services/calling-service/src/clients/twilio.ts
- Webhook routes:
services/calling-service/src/routes/twiml.ts
- Test scripts:
services/calling-service/tests/
External Resources
Remember: Thorough testing prevents costly production issues. Test every scenario, document every issue, and always prioritize call quality and compliance.