Integrate Voice AI with Salesforce for CRM Sync: My Implementation Journey

Discover how I integrated Voice AI with Salesforce for real-time CRM sync. Learn practical steps and tools like Twilio and VAPI for seamless setup.

Misal Azeem
Misal Azeem

Voice AI Engineer & Creator

Integrate Voice AI with Salesforce for CRM Sync: My Implementation Journey

Integrate Voice AI with Salesforce for CRM Sync: My Implementation Journey

TL;DR

Most voice AI integrations fail when CRM data isn't synced in real-time. Here's what breaks: webhook timeouts, race conditions between transcription and database writes, and missing call context during handoffs. This implementation uses VAPI for conversational AI agents, Twilio for carrier integration, and OAuth 2.0 to authenticate Salesforce writes. Result: live call transcription synced to contact records, zero manual data entry, sub-500ms latency on CRM updates.

Prerequisites

API Keys & Credentials

You'll need a VAPI API key (generate from your VAPI dashboard) and a Twilio Account SID + Auth Token for phone integration. Salesforce requires OAuth 2.0 credentials: Client ID, Client Secret, and a registered Connected App in your Salesforce org (Setup → Apps → App Manager → New Connected App).

System Requirements

Node.js 16+ with npm or yarn. A publicly accessible server (ngrok for local development, or AWS Lambda/Heroku for production). HTTPS is mandatory—Salesforce and VAPI reject HTTP webhooks.

Salesforce Configuration

Enable API access in your Salesforce org and create a custom object or use standard objects (Accounts, Contacts, Opportunities) for CRM sync. Ensure your user has API-enabled permissions.

Network & Security

Whitelist VAPI and Twilio IP ranges in your firewall. Generate a webhook secret for signature validation. Have process.env configured for all sensitive credentials.

VAPI: Get Started with VAPI → Get VAPI

Step-by-Step Tutorial

Configuration & Setup

First, configure your VAPI assistant to handle Salesforce queries. The assistant needs OAuth credentials and function calling enabled to query CRM data in real-time.

javascript
const assistantConfig = {
  model: {
    provider: "openai",
    model: "gpt-4",
    messages: [{
      role: "system",
      content: "You are a sales assistant with access to Salesforce CRM. When asked about leads, opportunities, or accounts, use the querySalesforce function to fetch real-time data."
    }],
    functions: [{
      name: "querySalesforce",
      description: "Query Salesforce CRM for leads, opportunities, or account data",
      parameters: {
        type: "object",
        properties: {
          objectType: { type: "string", enum: ["Lead", "Opportunity", "Account"] },
          filters: { type: "object" }
        },
        required: ["objectType"]
      }
    }]
  },
  voice: {
    provider: "11labs",
    voiceId: "21m00Tcm4TlvDq8ikWAM"
  },
  transcriber: {
    provider: "deepgram",
    model: "nova-2",
    language: "en"
  },
  serverUrl: process.env.WEBHOOK_URL, // Your server receives function calls here
  serverUrlSecret: process.env.VAPI_SERVER_SECRET
};

Critical: The serverUrl is YOUR webhook endpoint where VAPI sends function call requests. This is NOT a VAPI API endpoint—it's your Express/Fastify server that bridges VAPI to Salesforce.

Architecture & Flow

mermaid
flowchart LR
    A[User Voice Input] --> B[VAPI Assistant]
    B --> C{Function Call Needed?}
    C -->|Yes| D[Your Webhook Server]
    D --> E[Salesforce OAuth]
    E --> F[Salesforce REST API]
    F --> G[CRM Data]
    G --> D
    D --> B
    C -->|No| B
    B --> H[Voice Response]

VAPI handles voice transcription and synthesis. Your server handles Salesforce authentication and data queries. Keep these responsibilities separate—don't try to make VAPI call Salesforce directly.

Step-by-Step Implementation

Step 1: Salesforce OAuth Setup

Generate OAuth credentials in Salesforce Setup → App Manager → New Connected App. You need client_id, client_secret, and a refresh token. Store these in environment variables—never hardcode credentials.

Step 2: Webhook Handler for Function Calls

javascript
// YOUR server receives function calls from VAPI here
app.post('/webhook/vapi', async (req, res) => {
  const { message } = req.body;
  
  // Validate webhook signature (production requirement)
  const signature = req.headers['x-vapi-signature'];
  if (!validateSignature(signature, req.body, process.env.VAPI_SERVER_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  if (message.type === 'function-call') {
    const { functionCall } = message;
    
    try {
      // Get fresh Salesforce access token
      const sfToken = await refreshSalesforceToken();
      
      // Query Salesforce based on function parameters
      const query = buildSOQLQuery(functionCall.parameters);
      const sfResponse = await fetch(`https://yourinstance.salesforce.com/services/data/v58.0/query?q=${encodeURIComponent(query)}`, {
        headers: {
          'Authorization': `Bearer ${sfToken}`,
          'Content-Type': 'application/json'
        }
      });
      
      if (!sfResponse.ok) throw new Error(`Salesforce API error: ${sfResponse.status}`);
      
      const data = await sfResponse.json();
      
      // Return formatted results to VAPI
      return res.json({
        result: formatCRMData(data.records)
      });
      
    } catch (error) {
      console.error('Salesforce query failed:', error);
      return res.json({
        result: "I'm having trouble accessing CRM data right now. Please try again."
      });
    }
  }
  
  res.status(200).end();
});

Step 3: Token Refresh Logic

Salesforce access tokens expire after 2 hours. Implement automatic refresh using the refresh token—don't wait for 401 errors. Cache the access token in memory with a 90-minute TTL to avoid unnecessary OAuth calls.

Error Handling & Edge Cases

Race condition: If multiple calls query Salesforce simultaneously, you'll hit API rate limits (15,000 requests/24hrs for Enterprise). Implement request queuing with a 100ms delay between queries.

Partial data: Salesforce queries can return incomplete records if fields are null. Always check record.hasOwnProperty(field) before accessing—missing fields cause the assistant to hallucinate data.

Network timeouts: Salesforce API can take 2-5 seconds for complex queries. Set webhook timeout to 10 seconds minimum, or VAPI will drop the connection and the user hears silence.

System Diagram

Audio processing pipeline from microphone input to speaker output.

mermaid
graph LR
    Mic[Microphone Input]
    Buffer[Audio Buffer]
    VAD[Voice Activity Detection]
    STT[Speech-to-Text Engine]
    NLU[Intent Detection Module]
    API[External API Integration]
    DB[Database Access]
    LLM[Response Generation]
    TTS[Text-to-Speech Engine]
    Speaker[Speaker Output]
    Error[Error Handling]

    Mic --> Buffer
    Buffer --> VAD
    VAD -->|Voice Detected| STT
    VAD -->|Silence| Error
    STT --> NLU
    NLU -->|Intent Identified| LLM
    NLU -->|No Intent| Error
    LLM --> API
    LLM --> DB
    API --> LLM
    DB --> LLM
    LLM --> TTS
    TTS --> Speaker
    Error --> Speaker

Testing & Validation

Local Testing

Most integrations break because webhooks fail silently. Test locally with ngrok before deploying.

javascript
// Start ngrok tunnel for local webhook testing
// Terminal: ngrok http 3000

// Test webhook signature validation
const crypto = require('crypto');

function validateWebhook(req) {
  const signature = req.headers['x-vapi-signature'];
  const payload = JSON.stringify(req.body);
  const secret = process.env.VAPI_SERVER_SECRET;
  
  const hash = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  if (hash !== signature) {
    throw new Error('Invalid webhook signature');
  }
  return true;
}

// Test Salesforce token refresh
async function testSFConnection() {
  try {
    const sfToken = await refreshSalesforceToken();
    const response = await fetch(`${process.env.SF_INSTANCE_URL}/services/data/v58.0/sobjects/Contact`, {
      headers: { 'Authorization': `Bearer ${sfToken}` }
    });
    console.log('SF Connection:', response.ok ? 'Valid' : 'Failed');
  } catch (error) {
    console.error('SF Test Failed:', error.message);
  }
}

This will bite you: Webhook signatures expire after 5 minutes. If your server processes slowly, validation fails even with correct secrets.

Webhook Validation

Test function calls with curl before connecting VAPI. Most failures happen because the response format doesn't match what VAPI expects.

bash
# Test function call endpoint
curl -X POST https://your-ngrok-url.ngrok.io/webhook/vapi \
  -H "Content-Type: application/json" \
  -H "x-vapi-signature: test_signature" \
  -d '{
    "message": {
      "type": "function-call",
      "functionCall": {
        "name": "querySalesforce",
        "parameters": {
          "objectType": "Contact",
          "query": "John Doe"
        }
      }
    }
  }'

Check response codes: 200 with { result: {...} } means success. 401 = signature failed. 500 = your Salesforce query broke. Log the full sfResponse object to debug field mapping issues.

Real-World Example

Barge-In Scenario

User calls to update an opportunity. Agent starts reading back the current deal stage: "Your opportunity with Acme Corp is currently in the Negotiation phase with a value of—" User interrupts: "No, mark it closed-won."

Here's what happens under the hood. VAPI's transcriber fires a transcript.partial event the moment it detects speech energy during agent playback:

javascript
// Webhook handler receives barge-in event
app.post('/webhook/vapi', async (req, res) => {
  const { type, transcript, call } = req.body;
  
  if (type === 'transcript.partial' && transcript.text.includes('closed')) {
    // Cancel TTS immediately - don't wait for full transcript
    try {
      const response = await fetch(`https://api.vapi.ai/call/${call.id}/control`, {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer ' + process.env.VAPI_API_KEY,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ 
          action: 'interrupt',
          flushAudioBuffer: true // Critical: prevents old audio from playing
        })
      });
      
      if (!response.ok) {
        console.error(`Interrupt failed: ${response.status}`);
        return res.status(200).send(); // ACK webhook anyway
      }
      
      // Queue Salesforce update while STT completes
      const sfUpdate = {
        StageName: 'Closed Won',
        CloseDate: new Date().toISOString().split('T')[0]
      };
      
      // Fire-and-forget to avoid blocking response
      updateSalesforceOpportunity(call.metadata.opportunityId, sfUpdate)
        .catch(error => console.error('SF update failed:', error));
        
    } catch (error) {
      console.error('Barge-in handling error:', error);
    }
  }
  
  res.status(200).send();
});

The transcript.partial event arrives 180-250ms after speech starts. If you wait for transcript.final, you add 400-600ms latency—user hears the agent talking over them.

Event Logs

Real event sequence from production (timestamps in ms since call start):

json
{
  "events": [
    { "timestamp": 2847, "type": "transcript.partial", "text": "no mark" },
    { "timestamp": 2891, "type": "control.interrupt", "status": "TTS cancelled" },
    { "timestamp": 3104, "type": "transcript.partial", "text": "no mark it closed" },
    { "timestamp": 3389, "type": "transcript.final", "text": "No, mark it closed-won" },
    { "timestamp": 3405, "type": "function.call", "name": "updateOpportunity", "args": { "stage": "Closed Won" } },
    { "timestamp": 4201, "type": "function.result", "data": { "success": true, "updated": "Opp-12847" } },
    { "timestamp": 4218, "type": "assistant.message", "text": "Done. Marked as closed-won." }
  ]
}

Notice the 44ms gap between interrupt and partial update. That's the race condition window where duplicate audio can leak through if you don't cancel synchronously.

Edge Cases

Multiple rapid interrupts: User says "No wait—actually—mark it closed-won." Three transcript.partial events fire within 800ms. Solution: debounce with a 200ms window and only process the final complete phrase.

javascript
let lastInterruptTime = 0;
const DEBOUNCE_MS = 200;

if (type === 'transcript.partial') {
  const now = Date.now();
  if (now - lastInterruptTime < DEBOUNCE_MS) {
    return res.status(200).send(); // Ignore rapid-fire partials
  }
  lastInterruptTime = now;
  
  // Process interrupt only if text is meaningful
  if (transcript.text.trim().length < 3) {
    return res.status(200).send(); // Ignore noise/rustling
  }
  
  // Proceed with interrupt logic...
}

False positive from background noise: Phone rustling triggers VAD. The partial transcript is empty or gibberish like "uh". Guard against this with the length check above.

Salesforce API timeout during barge-in: The SF update takes 2.1s but user expects instant confirmation. Return optimistic response immediately, retry SF update in background with exponential backoff. If SF fails after 3 retries, queue for manual review rather than blocking the call.

Common Issues & Fixes

OAuth Token Expiration Mid-Call

Salesforce access tokens expire after 2 hours. If your voice agent runs long sessions, you'll hit 401 INVALID_SESSION_ID errors mid-conversation. This breaks CRM sync silently—the call continues but data never reaches Salesforce.

javascript
// Token refresh with expiration tracking
let sfToken = null;
let tokenExpiry = 0;

async function getSalesforceToken() {
  const now = Date.now();
  
  // Refresh 5 minutes before expiry
  if (sfToken && now < tokenExpiry - 300000) {
    return sfToken;
  }
  
  try {
    const response = await fetch('https://login.salesforce.com/services/oauth2/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: process.env.SF_CLIENT_ID,
        client_secret: process.env.SF_CLIENT_SECRET,
        refresh_token: process.env.SF_REFRESH_TOKEN
      })
    });
    
    if (!response.ok) {
      throw new Error(`Token refresh failed: ${response.status}`);
    }
    
    const data = await response.json();
    sfToken = data.access_token;
    tokenExpiry = now + (data.expires_in * 1000);
    
    return sfToken;
  } catch (error) {
    console.error('OAuth refresh error:', error);
    throw error;
  }
}

Fix: Implement token refresh logic that checks expiration before every Salesforce API call. Store expires_in from the OAuth response and refresh 5 minutes early to avoid race conditions during active calls.

Webhook Signature Validation Failures

Vapi sends webhook signatures in the x-vapi-signature header, but many developers validate against the wrong payload format. If you stringify the body incorrectly, signatures will never match—causing all webhooks to be rejected as unauthorized.

javascript
// Correct signature validation
function validateWebhook(req) {
  const signature = req.headers['x-vapi-signature'];
  const secret = process.env.VAPI_SERVER_SECRET;
  
  // CRITICAL: Use raw body buffer, not parsed JSON
  const payload = req.rawBody || JSON.stringify(req.body);
  
  const hash = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  if (hash !== signature) {
    throw new Error('Invalid webhook signature');
  }
}

Fix: Use the raw request body buffer for HMAC validation, not the parsed JSON object. Express.js parses bodies by default—you need express.raw() middleware to preserve the original buffer for signature checks.

Race Conditions on Rapid Function Calls

When users interrupt the AI mid-sentence, Vapi fires multiple function-call events in quick succession. Without debouncing, your server makes duplicate Salesforce API calls—creating multiple Contact records for the same person or logging the same call activity twice.

javascript
// Debounce rapid function calls
const lastInterruptTime = {};
const DEBOUNCE_MS = 500;

app.post('/webhook/vapi', async (req, res) => {
  const { type, call } = req.body;
  
  if (type === 'function-call') {
    const now = Date.now();
    const callId = call.id;
    
    // Reject if called within debounce window
    if (lastInterruptTime[callId] && 
        now - lastInterruptTime[callId] < DEBOUNCE_MS) {
      return res.json({ result: 'debounced' });
    }
    
    lastInterruptTime[callId] = now;
    
    // Process Salesforce update
    const sfUpdate = await updateSalesforceRecord(call);
    res.json({ result: sfUpdate });
  }
});

Fix: Track the last function call timestamp per call.id and reject requests within a 500ms window. Clean up old timestamps after call completion to prevent memory leaks in long-running servers.

Complete Working Example

This is the full production server that handles OAuth, webhooks, and Salesforce sync. Copy-paste this into server.js and you have a working system.

Full Server Code

javascript
// server.js - Complete VAPI + Salesforce integration
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
require('dotenv').config();

const app = express();
app.use(express.json());

// Session store for OAuth state and tokens
const sessions = new Map();
const TOKEN_TTL = 3600000; // 1 hour

// Salesforce OAuth endpoints
const SF_AUTH_URL = 'https://login.salesforce.com/services/oauth2/authorize';
const SF_TOKEN_URL = 'https://login.salesforce.com/services/oauth2/token';
const SF_API_BASE = `${process.env.SF_INSTANCE_URL}/services/data/v58.0`;

// OAuth flow: Step 1 - Redirect to Salesforce
app.get('/oauth/login', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex');
  sessions.set(state, { timestamp: Date.now() });
  
  const authUrl = `${SF_AUTH_URL}?response_type=code&client_id=${process.env.SF_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.SF_REDIRECT_URI)}&state=${state}`;
  res.redirect(authUrl);
});

// OAuth flow: Step 2 - Handle callback and exchange code for token
app.get('/oauth/callback', async (req, res) => {
  const { code, state } = req.query;
  
  if (!sessions.has(state)) {
    return res.status(400).send('Invalid state parameter');
  }
  
  try {
    const tokenResponse = await axios.post(SF_TOKEN_URL, new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      client_id: process.env.SF_CLIENT_ID,
      client_secret: process.env.SF_CLIENT_SECRET,
      redirect_uri: process.env.SF_REDIRECT_URI
    }), {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    });
    
    const sfToken = tokenResponse.data.access_token;
    const tokenExpiry = Date.now() + TOKEN_TTL;
    sessions.set('sf_token', { token: sfToken, expiry: tokenExpiry });
    
    res.send('Salesforce connected. You can close this window.');
  } catch (error) {
    console.error('OAuth error:', error.response?.data || error.message);
    res.status(500).send('Authentication failed');
  }
});

// Webhook signature validation
function validateWebhook(req) {
  const signature = req.headers['x-vapi-signature'];
  if (!signature) return false;
  
  const payload = JSON.stringify(req.body);
  const secret = process.env.VAPI_SERVER_SECRET;
  const hash = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hash));
}

// Main webhook handler - processes function calls from VAPI
app.post('/webhook', async (req, res) => {
  if (!validateWebhook(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  const { message } = req.body;
  
  // Handle function call from VAPI assistant
  if (message?.type === 'function-call') {
    const { name, parameters } = message.functionCall;
    
    try {
      const tokenData = sessions.get('sf_token');
      if (!tokenData || Date.now() > tokenData.expiry) {
        return res.json({ 
          result: { error: 'Salesforce token expired. Please re-authenticate.' }
        });
      }
      
      const sfToken = tokenData.token;
      
      if (name === 'query_salesforce') {
        // Execute SOQL query
        const query = parameters.query;
        const sfResponse = await axios.get(`${SF_API_BASE}/query`, {
          params: { q: query },
          headers: { 'Authorization': `Bearer ${sfToken}` }
        });
        
        return res.json({ result: sfResponse.data.records });
      }
      
      if (name === 'update_opportunity') {
        // Update Salesforce Opportunity
        const { opportunityId, stageName } = parameters;
        const sfUpdate = await axios.patch(
          `${SF_API_BASE}/sobjects/Opportunity/${opportunityId}`,
          { StageName: stageName },
          { headers: { 
            'Authorization': `Bearer ${sfToken}`,
            'Content-Type': 'application/json'
          }}
        );
        
        return res.json({ result: { success: true, status: sfUpdate.status } });
      }
      
      res.json({ result: { error: 'Unknown function' } });
    } catch (error) {
      console.error('Salesforce API error:', error.response?.data || error.message);
      res.json({ result: { error: error.message, failed: true } });
    }
  } else {
    // Log other webhook events (call status, transcripts, etc.)
    console.log('Webhook event:', message?.type);
    res.sendStatus(200);
  }
});

// Health check
app.get('/health', (req, res) => {
  const tokenData = sessions.get('sf_token');
  res.json({ 
    status: 'ok',
    salesforce_connected: tokenData && Date.now() < tokenData.expiry
  });
});

// Cleanup expired sessions every 10 minutes
setInterval(() => {
  const now = Date.now();
  for (const [key, value] of sessions.entries()) {
    if (value.expiry && now > value.expiry) {
      sessions.delete(key);
    }
  }
}, 600000);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  console.log(`OAuth URL: http://localhost:${PORT}/oauth/login`);
});

Run Instructions

Environment variables (.env):

SF_CLIENT_ID=your_salesforce_connected_app_id SF_CLIENT_SECRET=your_salesforce_connected_app_secret SF_REDIRECT_URI=https://your-domain.ngrok.io/oauth/callback SF_INSTANCE_URL=https://your-instance.salesforce.com VAPI_SERVER_SECRET=your_vapi_webhook_secret PORT=3000

Start the server:

bash
npm install express axios dotenv
node server.js

Expose with ngrok:

bash
ngrok http 3000

Connect Salesforce: Visit https://your-ngrok-url.ngrok.io/oauth/login to authorize. The token persists for 1 hour with automatic expiry handling.

Configure VAPI webhook: Set Server URL to https://your-ngrok-url.ngrok.io/webhook and paste your VAPI_SERVER_SECRET.

This handles OAuth refresh, signature validation, and Salesforce API errors. The session store prevents token leaks and cleans up expired credentials automatically.

FAQ

Technical Questions

How do I authenticate VAPI calls with Salesforce OAuth without exposing credentials?

Use the OAuth 2.0 authorization code flow. Store sfToken (access token) server-side in encrypted sessions, never in client-side code. When VAPI triggers a webhook, your server validates the request signature using validateWebhook(), then uses the stored token to query Salesforce. Refresh tokens before expiry (typically 2 hours) by checking tokenExpiry against current time. Never pass Salesforce credentials directly to VAPI—always proxy through your backend.

What happens if the Salesforce API call fails mid-conversation?

Implement retry logic with exponential backoff. If sfResponse.status returns 401 (token expired), call getSalesforceToken() to refresh. For 503 (service unavailable), queue the update and retry after 30 seconds. For 400 (bad request), log the error and continue the call—don't break the user experience. Store failed updates in a database and sync them asynchronously after the call ends.

Can I sync call transcripts to Salesforce in real-time?

Yes, but with latency trade-offs. Use VAPI's onPartialTranscript webhook to send partial transcripts as they arrive (every 500-800ms). This creates ~1-2 second lag in Salesforce. For final transcripts, wait until call completion and batch-update using sfUpdate. Avoid sending every character—batch updates every 3-5 seconds to reduce API quota burn.

Performance

How many concurrent calls can I handle before hitting Salesforce API limits?

Salesforce allows 10,000 API calls per 24 hours on most plans. Each call (query + update) costs 2 API calls. At 100 concurrent calls, you'll hit limits in ~50 hours. Implement call queuing: buffer updates in Redis or PostgreSQL, then batch-sync every 60 seconds. This reduces API calls by 80% and prevents throttling (429 errors).

What's the latency impact of syncing CRM data during a call?

Expect 200-400ms added latency per Salesforce query. Use connection pooling and cache frequently-accessed records (accounts, contacts) for 5 minutes. Parallel queries (fetch account + contact simultaneously) reduce latency by 40%. For real-time queries, pre-fetch data before the call starts using VAPI's metadata field.

Platform Comparison

Should I use Twilio or VAPI for voice handling?

VAPI handles voice infrastructure natively (STT, TTS, VAD). Twilio provides carrier-grade telephony and PSTN integration. Use VAPI for AI-driven conversations; use Twilio if you need inbound phone numbers or SIP trunking. You can bridge both: Twilio receives the call, forwards to VAPI via WebSocket, VAPI handles the AI logic. This adds ~100ms latency but gives you carrier reliability + AI intelligence.

How do I handle barge-in (interruption) while syncing Salesforce data?

Implement debouncing with DEBOUNCE_MS (set to 300ms). When user interrupts, cancel pending Salesforce queries using AbortController. Store lastInterruptTime and ignore updates within the debounce window. This prevents race conditions where old Salesforce data overwrites new user input. Test with high-latency networks (3G) to catch edge cases.

Resources

Twilio: Get Twilio Voice API → https://www.twilio.com/try-twilio

Official Documentation

  • VAPI Voice AI API – Real-time conversational AI agents, webhook integration, function calling
  • Salesforce REST API – OAuth 2.0 authentication, SOQL queries, record updates
  • Twilio Voice API – SIP integration, call routing, webhook callbacks

Implementation References

  • VAPI function calling for CRM queries – enables dynamic Salesforce lookups during calls
  • Salesforce OAuth 2.0 flow – secure token management for API access
  • Webhook signature validation – protects against unauthorized call events

References

  1. https://docs.vapi.ai/quickstart/phone
  2. https://docs.vapi.ai/quickstart/web
  3. https://docs.vapi.ai/quickstart/introduction
  4. https://docs.vapi.ai/assistants/quickstart
  5. https://docs.vapi.ai/chat/quickstart
  6. https://docs.vapi.ai/workflows/quickstart
  7. https://docs.vapi.ai/outbound-campaigns/quickstart
  8. https://docs.vapi.ai/observability/evals-quickstart

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.