Advertisement
Table of Contents
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
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:
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
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:
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:
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:
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:
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:
- FCR test: Call, resolve issue in 1 turn, verify
fcr: true - Escalation test: Trigger transfer, verify
fcr: false, escalated: true - AHT test: 90-second call, compare VAPI duration vs Twilio duration (should match ±2s)
- 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.
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:
ngrok http 3000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
Update your VAPI assistant config with the ngrok URL:
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:
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:
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:
// 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:
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.
// 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
// 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):
VAPI_SERVER_SECRET=your_webhook_secret_from_vapi_dashboard
TWILIO_ACCOUNT_SID=ACxxxx
TWILIO_AUTH_TOKEN=your_twilio_auth_token
Install dependencies:
npm install express
Start server:
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
- VAPI Call Analytics API – Track call metadata, duration, and transcripts
- Twilio Call Logs REST API – Retrieve call duration, status, and recording data
- VAPI Webhooks – Real-time call events for FCR and deflection tracking
Integration Patterns
- VAPI metadata field stores
businessUnit,sentiment,turnsfor outcome classification - Twilio
CallResourcereturnsduration,status,pricefor AHT and cost analysis - Webhook payloads include
call.endedReasonto 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
- https://docs.vapi.ai/outbound-campaigns/quickstart
- https://docs.vapi.ai/observability/evals-quickstart
- https://docs.vapi.ai/assistants/structured-outputs-quickstart
- https://docs.vapi.ai/observability/boards-quickstart
- https://docs.vapi.ai/workflows/quickstart
Advertisement
Written by
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.
Found this helpful?
Share it with other developers building voice AI.



