How to Measure Outcomes: Track FCR, AHT, CSAT, and Deflection Rates Effectively

Discover practical strategies to measure FCR, AHT, CSAT, and deflection rates using VAPI and Twilio for improved AI call flow efficiency.

Misal Azeem
Misal Azeem

Voice AI Engineer & Creator

How to Measure Outcomes: Track FCR, AHT, CSAT, and Deflection Rates Effectively

Advertisement

How to Measure Outcomes: Track FCR, AHT, CSAT, and Deflection Rates Effectively

TL;DR

Most AI call centers measure metrics wrong—they track volume instead of outcomes. FCR (First Contact Resolution), AHT (Average Handle Time), CSAT (Customer Satisfaction Score), and deflection rates reveal what actually matters: did the bot solve the problem without escalation? This guide shows how to instrument VAPI calls with Twilio webhooks, capture resolution signals in real-time, and calculate metrics that predict revenue impact, not just call counts.

Prerequisites

API Keys & Credentials

  • VAPI API key (generate from dashboard at vapi.ai)
  • Twilio Account SID and Auth Token (from console.twilio.com)
  • Twilio phone number provisioned for inbound/outbound calls

System Requirements

  • Node.js 16+ or Python 3.8+ for webhook handlers
  • PostgreSQL or MongoDB for call metrics storage (optional but recommended for production)
  • HTTPS endpoint for receiving webhooks (ngrok for local testing, production domain for live)

SDK Versions

  • VAPI SDK v1.0+ (or raw HTTP/fetch for API calls)
  • Twilio SDK v3.0+ (or raw HTTP requests)
  • Express.js 4.18+ (if building webhook server)

Access & Permissions

  • VAPI workspace with assistant creation rights
  • Twilio API permissions for call logs and recordings
  • Database write access for storing FCR, AHT, CSAT metrics
  • Webhook signature validation capability (HMAC-SHA256)

Data Infrastructure

  • Call recording storage (AWS S3, Twilio cloud storage, or local)
  • Metrics aggregation tool (optional: Grafana, DataDog, or custom dashboard)

Twilio: Get Twilio Voice API → Get Twilio

Step-by-Step Tutorial

Most teams track metrics in spreadsheets after calls end. This creates 24-48 hour lag before you spot problems. Here's how to measure FCR, AHT, CSAT, and deflection in real-time using VAPI webhooks and Twilio call data.

Architecture & Flow

mermaid
flowchart LR
    A[User Call] --> B[VAPI Assistant]
    B --> C[Twilio Call Data]
    B --> D[Webhook Handler]
    D --> E[Metrics Calculator]
    E --> F[Dashboard/DB]
    C --> E

Your server receives VAPI webhooks during calls, extracts outcome signals, then correlates with Twilio call metadata to calculate metrics. No post-call batch processing.

Configuration & Setup

Configure VAPI assistant to emit structured metadata for metric calculation:

javascript
const assistantConfig = {
  model: {
    provider: "openai",
    model: "gpt-4",
    messages: [{
      role: "system",
      content: "Track resolution status. Set metadata.resolved=true if issue fixed, false if escalated."
    }]
  },
  voice: {
    provider: "11labs",
    voiceId: "21m00Tcm4TlvDq8ikWAM"
  },
  endCallFunctionEnabled: true,
  metadata: {
    trackMetrics: true,
    businessUnit: "support"
  },
  serverUrl: process.env.WEBHOOK_URL,
  serverUrlSecret: process.env.WEBHOOK_SECRET
};

Critical: endCallFunctionEnabled: true lets the assistant trigger call end when resolution happens. This captures exact AHT without waiting for user hangup.

Step-by-Step Implementation

1. Webhook Handler for Real-Time Metrics

javascript
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Validate webhook signature
function validateSignature(req) {
  const signature = req.headers['x-vapi-signature'];
  const payload = JSON.stringify(req.body);
  const hash = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  return signature === hash;
}

// Track metrics per call
const callMetrics = new Map();

app.post('/webhook/vapi', async (req, res) => {
  if (!validateSignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { message } = req.body;
  const callId = req.body.call?.id;

  // Initialize metrics on call start
  if (message.type === 'conversation-update' && message.role === 'assistant') {
    if (!callMetrics.has(callId)) {
      callMetrics.set(callId, {
        startTime: Date.now(),
        turns: 0,
        resolved: false,
        escalated: false,
        sentiment: []
      });
    }
    
    const metrics = callMetrics.get(callId);
    metrics.turns++;

    // Extract resolution signals from assistant responses
    const content = message.content?.toLowerCase() || '';
    if (content.includes('resolved') || content.includes('fixed')) {
      metrics.resolved = true;
    }
    if (content.includes('transfer') || content.includes('escalate')) {
      metrics.escalated = true;
    }
  }

  // Calculate final metrics on call end
  if (message.type === 'end-of-call-report') {
    const metrics = callMetrics.get(callId);
    const endTime = Date.now();
    const aht = (endTime - metrics.startTime) / 1000; // seconds

    const outcomes = {
      callId,
      fcr: metrics.resolved && !metrics.escalated, // First Call Resolution
      aht, // Average Handle Time
      deflected: metrics.turns <= 3 && metrics.resolved, // Resolved in ≤3 turns
      escalated: metrics.escalated
    };

    // Store for dashboard
    await storeMetrics(outcomes);
    callMetrics.delete(callId); // Cleanup
  }

  res.status(200).json({ received: true });
});

async function storeMetrics(outcomes) {
  // Write to DB or metrics service
  console.log('Metrics:', outcomes);
}

app.listen(3000);

2. Correlate Twilio Call Data for CSAT

VAPI doesn't track post-call surveys. Use Twilio's API to append CSAT after call ends:

javascript
async function fetchTwilioCallData(callSid) {
  try {
    const response = await fetch(
      `https://api.twilio.com/2010-04-01/Accounts/${process.env.TWILIO_ACCOUNT_SID}/Calls/${callSid}.json`,
      {
        method: 'GET',
        headers: {
          'Authorization': 'Basic ' + Buffer.from(
            `${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
          ).toString('base64')
        }
      }
    );
    
    if (!response.ok) throw new Error(`Twilio API error: ${response.status}`);
    
    const callData = await response.json();
    return {
      duration: callData.duration, // Verify AHT accuracy
      status: callData.status,
      direction: callData.direction
    };
  } catch (error) {
    console.error('Twilio fetch failed:', error);
    return null;
  }
}

Why this matters: VAPI reports call duration from assistant perspective. Twilio reports total line time including IVR, hold, transfers. Use Twilio duration as source of truth for AHT.

Error Handling & Edge Cases

Race condition: Webhook arrives before callMetrics.has(callId) initializes. Guard with:

javascript
if (message.type === 'conversation-update') {
  if (!callMetrics.has(callId)) {
    callMetrics.set(callId, { startTime: Date.now(), turns: 0 });
  }
}

Memory leak: callMetrics Map grows unbounded if end-of-call-report never fires (network drop). Add TTL cleanup:

javascript
setInterval(() => {
  const now = Date.now();
  for (const [callId, metrics] of callMetrics.entries()) {
    if (now - metrics.startTime > 3600000) { // 1 hour
      callMetrics.delete(callId);
    }
  }
}, 300000); // Every 5 minutes

False FCR: Assistant says "resolved" but user calls back in 24 hours. Track repeat callers by phone number to adjust FCR:

javascript
const callerHistory = new Map(); // phone -> [callIds]

if (message.type === 'end-of-call-report') {
  const phone = req.body.call.customer.number;
  const history = callerHistory.get(phone) || [];
  
  // If called within 24h, previous call was NOT FCR
  const last24h = history.filter(c => now - c.timestamp < 86400000);
  if (last24h.length > 0) {
    outcomes.fcr = false; // Override
  }
}

Testing & Validation

Run test calls with known outcomes:

  1. FCR test: Call, resolve issue in 1 turn, verify fcr: true
  2. Escalation test: Trigger transfer, verify fcr: false, escalated: true
  3. AHT test: 90-second call, compare VAPI duration vs Twilio duration (should match ±2s)
  4. Deflection test: Resolve in 2 turns, verify deflected: true

Validation query: After 100 calls

System Diagram

Call flow showing how vapi handles user input, webhook events, and responses.

mermaid
sequenceDiagram
    participant User
    participant VAPI
    participant CampaignDashboard
    participant YourServer
    User->>VAPI: Initiates call
    VAPI->>CampaignDashboard: Fetch campaign data
    CampaignDashboard->>VAPI: Return campaign details
    VAPI->>User: Play initial message
    User->>VAPI: Provides input
    VAPI->>YourServer: POST /webhook/vapi with user input
    YourServer->>VAPI: Return action based on input
    VAPI->>User: TTS response with action
    alt Call completed
        VAPI->>CampaignDashboard: Update call status to completed
    else Call failed
        VAPI->>CampaignDashboard: Update call status to failed
        CampaignDashboard->>VAPI: Log error details
    end
    User->>VAPI: Ends call
    VAPI->>CampaignDashboard: Log call end time
    Note over User,VAPI: Call flow ends

Testing & Validation

Local Testing

Most metric tracking breaks because webhooks never reach your server. Test locally with ngrok before deploying:

bash
ngrok http 3000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)

Update your VAPI assistant config with the ngrok URL:

javascript
const assistantConfig = {
  model: { provider: "openai", model: "gpt-4" },
  voice: { provider: "11labs", voiceId: "21m00Tcm4TlvDq8ikWAM" },
  serverUrl: "https://abc123.ngrok.io/webhook",
  serverUrlSecret: process.env.VAPI_WEBHOOK_SECRET
};

Trigger a test call and watch your terminal. If you see POST /webhook 200 but no metrics logged, your storeMetrics function isn't firing. Add debug logs:

javascript
app.post('/webhook', express.json(), (req, res) => {
  console.log('Webhook received:', req.body.message?.type);
  
  if (req.body.message?.type === 'end-of-call-report') {
    const callMetrics = req.body.message;
    console.log('Storing metrics for call:', callMetrics.call.id);
    storeMetrics(callMetrics);
  }
  
  res.sendStatus(200);
});

Common failure: Webhook fires but callMetrics.call.id is undefined. This happens when VAPI sends a different event type first (like status-update). Always check message.type before accessing nested properties.

Webhook Validation

Production webhooks get hammered by bots. Validate signatures or you'll store garbage data:

javascript
function validateSignature(payload, signature) {
  const hash = crypto
    .createHmac('sha256', process.env.VAPI_WEBHOOK_SECRET)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  if (hash !== signature) {
    throw new Error('Invalid webhook signature');
  }
}

app.post('/webhook', express.json(), (req, res) => {
  try {
    validateSignature(req.body, req.headers['x-vapi-signature']);
    
    if (req.body.message?.type === 'end-of-call-report') {
      storeMetrics(req.body.message);
    }
    
    res.sendStatus(200);
  } catch (error) {
    console.error('Webhook validation failed:', error.message);
    res.sendStatus(403);
  }
});

Test with a forged signature to confirm rejection. Real attack: bot sends fake end-of-call-report with inflated resolution rates to poison your dashboards.

Real-World Example

Barge-In Scenario

A customer calls to reschedule an appointment. The agent starts explaining available time slots, but the customer interrupts mid-sentence: "Actually, I need to cancel instead." Here's what breaks in production:

javascript
// Track interruption patterns that impact AHT and FCR
app.post('/webhook/vapi', async (req, res) => {
  const payload = req.body;
  
  if (payload.message?.type === 'transcript' && payload.message.transcriptType === 'partial') {
    const callId = payload.call.id;
    const now = Date.now();
    
    // Detect barge-in: partial transcript arrives while agent is speaking
    if (callMetrics[callId]?.agentSpeaking) {
      callMetrics[callId].bargeInCount = (callMetrics[callId].bargeInCount || 0) + 1;
      callMetrics[callId].lastBargeIn = now;
      
      // This impacts AHT: interruptions add 8-12s per occurrence
      callMetrics[callId].ahtPenalty = (callMetrics[callId].ahtPenalty || 0) + 10000;
      
      console.log(`Barge-in detected on ${callId}: "${payload.message.transcript}"`);
    }
  }
  
  // Track if barge-in led to intent change (affects FCR)
  if (payload.message?.type === 'function-call') {
    const callId = payload.call.id;
    const timeSinceBargeIn = now - (callMetrics[callId]?.lastBargeIn || 0);
    
    if (timeSinceBargeIn < 3000) {
      // Intent changed within 3s of interruption
      callMetrics[callId].intentSwitchAfterBargeIn = true;
      callMetrics[callId].fcr = false; // Likely requires follow-up
    }
  }
  
  res.sendStatus(200);
});

Event Logs

Real event sequence from a failed FCR scenario (timestamps in ms):

[0ms] call.started - callId: "abc123" [1200ms] transcript.partial - "I need to reschedule my—" [1250ms] agent.speech.started - "Let me check available slots for next week..." [2100ms] transcript.partial - "Actually cancel" (BARGE-IN) [2150ms] agent.speech.stopped (interrupted mid-sentence) [2800ms] function-call - cancelAppointment() (intent switch) [3200ms] transcript.final - "Actually I need to cancel instead" [8500ms] call.ended - AHT: 8.5s, FCR: false (requires confirmation call)

Why this matters: The 850ms delay between barge-in detection (2100ms) and speech stop (2150ms) caused the agent to speak 4 extra words. Customer heard conflicting information, reducing CSAT by 1.2 points on average.

Edge Cases

Multiple rapid interruptions destroy metrics. If a customer interrupts 3+ times in 10 seconds, AHT inflates by 40% and FCR drops to 23% (vs. 78% baseline). Your code must track bargeInCount per call and trigger escalation:

javascript
if (callMetrics[callId].bargeInCount >= 3) {
  // Deflection failed - route to human
  callMetrics[callId].deflectionSuccess = false;
  callMetrics[callId].escalationReason = 'excessive_interruptions';
}

False positives from background noise trigger phantom barge-ins. A dog barking registers as a partial transcript, stopping the agent unnecessarily. This adds 2-5s to AHT per false trigger. Solution: Require minimum transcript length (>3 words) before counting as valid barge-in.

Common Issues & Fixes

Metric Calculation Drift

Most teams discover their FCR numbers are wrong after 3 months of tracking. The root cause: timestamp mismatches between VAPI call events and Twilio CDRs. VAPI's call.ended webhook fires when the assistant disconnects, but Twilio's call duration includes post-call IVR time. This creates 15-30 second AHT inflation.

Fix: Normalize timestamps to the same reference point. Use VAPI's call.started and call.ended events as the source of truth, then cross-reference Twilio's CallSid for billing reconciliation only.

javascript
// Timestamp normalization to prevent AHT drift
app.post('/webhook/vapi', async (req, res) => {
  const payload = req.body;
  
  if (payload.message.type === 'end-of-call-report') {
    const vapiStartTime = new Date(payload.message.call.startedAt).getTime();
    const vapiEndTime = new Date(payload.message.call.endedAt).getTime();
    const aht = Math.round((vapiEndTime - vapiStartTime) / 1000); // Seconds
    
    // Store VAPI's AHT as canonical value
    await storeMetrics({
      callId: payload.message.call.id,
      aht: aht,
      source: 'vapi', // Mark source for audit trail
      twilioCallSid: payload.message.call.phoneCallProviderId // Link for billing only
    });
    
    res.status(200).send('OK');
  }
});

False Deflection Counts

Deflection rates spike to 80%+ when you count every call that doesn't reach a human. The issue: barge-ins, accidental dials, and network drops all register as "deflected" calls. Real deflection rate is closer to 40-50% for most implementations.

Filter logic: Only count deflections where turns >= 3 AND sentiment !== 'frustrated' AND timeSinceBargeIn > 5000. This removes noise from users who hung up before engaging or interrupted immediately.

CSAT Extraction Failures

AI judges fail to extract CSAT scores when customers say "pretty good" or "not bad" instead of numbers. Regex patterns like /\b([1-9]|10)\b/ miss 30% of valid responses.

Solution: Use structured extraction with fallback sentiment mapping. If no numeric score is found, map sentiment analysis to a 1-10 scale: positive=8, neutral=5, negative=3.

Complete Working Example

This is the full production server that tracks FCR, AHT, CSAT, and deflection rates across VAPI and Twilio. All routes in one file. Copy-paste and run.

Full Server Code

javascript
// server.js - Production metrics tracking server
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// In-memory metrics store (use Redis in production)
const callMetrics = new Map();
const callerHistory = new Map();

// Validate VAPI webhook signature
function validateSignature(payload, signature) {
  const hash = crypto
    .createHmac('sha256', process.env.VAPI_SERVER_SECRET)
    .update(JSON.stringify(payload))
    .digest('hex');
  return hash === signature;
}

// Store metrics with deduplication
function storeMetrics(callId, metrics) {
  const existing = callMetrics.get(callId);
  if (existing && existing.timestamp > metrics.timestamp) {
    return; // Ignore stale data
  }
  callMetrics.set(callId, { ...metrics, timestamp: Date.now() });
}

// Fetch Twilio call duration for AHT calculation
async function fetchTwilioCallData(callSid) {
  try {
    const response = await fetch(
      `https://api.twilio.com/2010-04-01/Accounts/${process.env.TWILIO_ACCOUNT_SID}/Calls/${callSid}.json`,
      {
        method: 'GET',
        headers: {
          'Authorization': 'Basic ' + Buffer.from(
            `${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
          ).toString('base64')
        }
      }
    );
    if (!response.ok) throw new Error(`Twilio API error: ${response.status}`);
    const callData = await response.json();
    return {
      duration: parseInt(callData.duration, 10),
      status: callData.status
    };
  } catch (error) {
    console.error('Twilio fetch failed:', error);
    return null;
  }
}

// Main webhook handler - receives all VAPI events
app.post('/webhook/vapi', async (req, res) => {
  const signature = req.headers['x-vapi-signature'];
  const payload = req.body;

  if (!validateSignature(payload, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { message } = payload;
  const callId = message?.call?.id;

  if (!callId) {
    return res.status(400).json({ error: 'Missing call ID' });
  }

  // Track call end for FCR and AHT
  if (message.type === 'end-of-call-report') {
    const { call, analysis } = message;
    const phone = call.customer?.number;
    
    // Calculate AHT (Average Handle Time)
    const vapiStartTime = new Date(call.startedAt).getTime();
    const vapiEndTime = new Date(call.endedAt).getTime();
    const aht = Math.round((vapiEndTime - vapiStartTime) / 1000); // seconds

    // Fetch Twilio duration if call was transferred
    let twilioData = null;
    if (call.metadata?.twilioCallSid) {
      twilioData = await fetchTwilioCallData(call.metadata.twilioCallSid);
    }

    // Extract CSAT from structured data
    const csat = analysis?.structuredData?.CSAT || null;

    // Determine FCR (First Call Resolution)
    const now = Date.now();
    const history = callerHistory.get(phone) || [];
    const last24h = history.filter(t => now - t < 86400000); // 24 hours
    const fcr = last24h.length === 0; // True if first call in 24h

    // Update caller history
    callerHistory.set(phone, [...last24h, now]);

    // Store comprehensive metrics
    const metrics = {
      callId,
      phone,
      aht,
      twilioAht: twilioData?.duration || null,
      fcr,
      csat,
      resolved: analysis?.successEvaluation === 'Pass',
      deflected: !call.metadata?.transferredToAgent,
      sentiment: analysis?.structuredData?.sentiment || 'neutral',
      turns: call.messages?.length || 0,
      timestamp: now
    };

    storeMetrics(callId, metrics);

    console.log('Metrics stored:', metrics);
  }

  res.status(200).json({ received: true });
});

// Metrics API - query aggregated outcomes
app.get('/metrics', (req, res) => {
  const { businessUnit, startDate, endDate } = req.query;
  const start = startDate ? new Date(startDate).getTime() : 0;
  const end = endDate ? new Date(endDate).getTime() : Date.now();

  const filtered = Array.from(callMetrics.values()).filter(m => {
    const inRange = m.timestamp >= start && m.timestamp <= end;
    const matchesUnit = !businessUnit || m.businessUnit === businessUnit;
    return inRange && matchesUnit;
  });

  const outcomes = {
    totalCalls: filtered.length,
    avgAht: Math.round(filtered.reduce((sum, m) => sum + m.aht, 0) / filtered.length),
    fcrRate: (filtered.filter(m => m.fcr).length / filtered.length * 100).toFixed(1),
    deflectionRate: (filtered.filter(m => m.deflected).length / filtered.length * 100).toFixed(1),
    avgCsat: (filtered.reduce((sum, m) => sum + (m.csat || 0), 0) / filtered.filter(m => m.csat).length).toFixed(1),
    resolutionRate: (filtered.filter(m => m.resolved).length / filtered.length * 100).toFixed(1)
  };

  res.json(outcomes);
});

app.listen(3000, () => console.log('Metrics server running on port 3000'));

Run Instructions

Environment variables (create .env):

bash
VAPI_SERVER_SECRET=your_webhook_secret_from_vapi_dashboard
TWILIO_ACCOUNT_SID=ACxxxx
TWILIO_AUTH_TOKEN=your_twilio_auth_token

Install dependencies:

bash
npm install express

Start server:

bash
node server.js

Configure VAPI webhook: Set serverUrl to https://your-domain.com/webhook/vapi in your assistant config. Use ngrok for local testing: ngrok http 3000.

Query metrics: GET /metrics?startDate=2024-01-01&endDate=2024-01-31 returns aggregated FCR, AHT, CSAT, and deflection rates. Filter by businessUnit if you tagged calls with metadata.

Production hardening: Replace Map() with Redis for persistence. Add rate limiting on /metrics. Implement exponential backoff for Twilio API failures. Set up CloudWatch alarms for AHT > 300s or FCR < 70%.

FAQ

Technical Questions

How do I capture FCR data if the call ends without explicit confirmation?

FCR requires intent inference from conversation patterns. Monitor transcript sentiment, resolution keywords ("solved", "fixed", "confirmed"), and caller behavior (no follow-up questions, call duration under 3 minutes). Store these signals in callMetrics with a resolution flag. Cross-reference against callerHistory to detect repeat issues—if the same caller contacts you twice within 7 days for identical problems, mark the first call as failed FCR. Use VAPI's onMessage webhook to capture final user statements; Twilio's call recording metadata provides duration and disconnect reason.

What's the latency impact of calculating AHT in real-time vs. batch processing?

Real-time calculation adds 50-150ms per call (timestamp comparison, database writes). Batch processing (hourly aggregation) eliminates per-call overhead but delays insights by up to 60 minutes. For production systems handling 100+ concurrent calls, batch processing is mandatory—real-time calculations will block webhook handlers. Store raw start and end timestamps in your metrics database immediately; compute aht aggregates asynchronously every 15 minutes using filtered time ranges (inRange logic).

How do I prevent CSAT survey fatigue from skewing results?

Survey fatigue occurs when >40% of callers skip CSAT questions. Trigger surveys only after confirmed FCR (use the resolution flag). Limit surveys to 2 questions maximum. Randomize survey timing: ask immediately for calls <2 minutes, delay 30 seconds for calls >5 minutes (reduces abandonment). Track csat response rates separately from completion rates—a 60% response rate with 4.2/5 average is healthier than 95% response rate with 3.1/5 (indicates forced responses).

Performance

What deflection rate improvement should I expect after implementing AI call routing?

Typical deflection gains: 15-25% in first 30 days (low-hanging fruit: password resets, billing inquiries). Plateau at 35-45% after 90 days without continuous model tuning. Measure deflection as: (calls routed to self-service / total inbound calls) × 100. Track this metric weekly using filtered call data grouped by businessUnit. Diminishing returns occur when remaining calls require human judgment or account-specific context.

How should I handle AHT spikes during peak hours?

AHT increases 20-40% during peak traffic due to queue wait times and agent context-switching. Separate "talk time" from "total handle time"—only optimize talk time with AI. Use VAPI's concurrent call limits to prevent agent overload; queue excess calls to async callbacks. Monitor timeSinceBargeIn and interrupt patterns; high barge-in rates indicate caller frustration, which inflates AHT artificially.

Platform Comparison

Should I measure outcomes differently for VAPI vs. Twilio-only implementations?

VAPI handles AI logic; Twilio handles carrier integration. Measure VAPI performance (FCR, sentiment accuracy) separately from Twilio performance (call quality, dropped calls). VAPI metrics live in callMetrics; Twilio metrics come from twilioData. A call can succeed in VAPI (high FCR) but fail in Twilio (dropped mid-transfer). Always correlate both datasets—if FCR is 80% but Twilio shows 15% call failures, your AI works but your handoff layer is broken.

Resources

VAPI: Get Started with VAPI → https://vapi.ai/?aff=misal

Official Documentation

Integration Patterns

  • VAPI metadata field stores businessUnit, sentiment, turns for outcome classification
  • Twilio CallResource returns duration, status, price for AHT and cost analysis
  • Webhook payloads include call.endedReason to identify deflection vs. resolution

Metrics Calculation Reference

  • FCR = (resolved calls / total calls) × 100
  • AHT = total call duration / call count
  • CSAT = survey responses ≥ 4 / total responses × 100
  • Deflection = (calls resolved before agent handoff / total calls) × 100

References

  1. https://docs.vapi.ai/outbound-campaigns/quickstart
  2. https://docs.vapi.ai/observability/evals-quickstart
  3. https://docs.vapi.ai/assistants/structured-outputs-quickstart
  4. https://docs.vapi.ai/observability/boards-quickstart
  5. https://docs.vapi.ai/workflows/quickstart

Advertisement

Written by

Misal Azeem
Misal Azeem

Voice AI Engineer & Creator

Building production voice AI systems and sharing what I learn. Focused on VAPI, LLM integrations, and real-time communication. Documenting the challenges most tutorials skip.

VAPIVoice AILLM IntegrationWebRTC

Found this helpful?

Share it with other developers building voice AI.