DEV Community

Cover image for Integrate Voice AI with Salesforce for Lead Qualification
CallStack Tech
CallStack Tech

Posted on • Originally published at callstack.tech

Integrate Voice AI with Salesforce for Lead Qualification

Integrate Voice AI with Salesforce for Lead Qualification

TL;DR

Most Salesforce voice integrations break when leads hang up mid-qualification—you lose partial data and can't resume. Here's how to build a VAPI voice agent that writes lead scores to Salesforce in real-time, handles interruptions, and triggers workflows based on qualification criteria. Stack: VAPI for voice AI, Twilio for telephony, Salesforce REST API for CRM writes. Outcome: Automated lead qualification that captures every interaction, even incomplete calls, and routes hot leads instantly to sales reps.

Prerequisites

API Access:

  • VAPI API key (from dashboard.vapi.ai)
  • Salesforce Connected App credentials (Consumer Key + Secret)
  • Twilio Account SID + Auth Token (for phone number provisioning)
  • OAuth 2.0 refresh token for Salesforce API (generate via Postman or curl)

System Requirements:

  • Node.js 18+ (for async/await and native fetch)
  • Public HTTPS endpoint (ngrok for dev, AWS Lambda/Vercel for prod)
  • Salesforce API v58.0+ (REST API access required)

Salesforce Setup:

  • Custom Lead fields: AI_Qualification_Score__c, Call_Transcript__c, Last_AI_Contact__c
  • Process Builder or Flow to route qualified leads (Score > 70)
  • API-enabled user profile with Lead read/write permissions

Knowledge:

  • OAuth 2.0 flow (authorization code grant)
  • Webhook signature validation (HMAC-SHA256)
  • Salesforce SOQL query syntax

vapi: Get Started with VAPI → Get vapi

Step-by-Step Tutorial

Most Salesforce voice integrations fail because they treat lead qualification as a simple webhook. Real production systems need bidirectional sync, real-time CRM updates during calls, and fallback logic when Salesforce APIs timeout. Here's how to build it correctly.

Configuration & Setup

First, configure your Vapi assistant with Salesforce-aware context. The assistant needs to understand lead qualification criteria before making API calls.

const assistantConfig = {
  model: {
    provider: "openai",
    model: "gpt-4",
    messages: [{
      role: "system",
      content: "You are a lead qualification specialist. Ask about: company size, budget, timeline, decision-maker status. Update Salesforce in real-time based on responses."
    }]
  },
  voice: {
    provider: "11labs",
    voiceId: "21m00Tcm4TlvDq8ikWAM"
  },
  transcriber: {
    provider: "deepgram",
    model: "nova-2",
    language: "en"
  },
  serverUrl: process.env.WEBHOOK_URL,
  serverUrlSecret: process.env.WEBHOOK_SECRET
};
Enter fullscreen mode Exit fullscreen mode

Configure Twilio for phone number provisioning. Vapi handles the voice layer; Twilio routes calls to your webhook endpoint.

const twilioConfig = {
  accountSid: process.env.TWILIO_ACCOUNT_SID,
  authToken: process.env.TWILIO_AUTH_TOKEN,
  phoneNumber: process.env.TWILIO_PHONE_NUMBER
};
Enter fullscreen mode Exit fullscreen mode

Architecture & Flow

Critical distinction: Vapi processes voice → text → AI responses. Your server bridges Vapi events to Salesforce APIs. Twilio only handles call routing.

flowchart LR
    A[Lead calls in] --> B[Twilio routes to Vapi]
    B --> C[Vapi transcribes speech]
    C --> D[Your webhook receives transcript]
    D --> E[Query Salesforce Lead API]
    E --> F[Update qualification score]
    F --> G[Return context to Vapi]
    G --> H[Vapi responds to lead]
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Implementation

1. Webhook Handler for Real-Time Updates

Your server receives Vapi events and syncs to Salesforce during the conversation—not after.

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

app.post('/webhook/vapi', async (req, res) => {
  // Validate webhook signature (production requirement)
  const signature = req.headers['x-vapi-signature'];
  const expectedSig = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (signature !== expectedSig) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { message } = req.body;

  // Handle transcript events for real-time qualification
  if (message.type === 'transcript') {
    const transcript = message.transcript;
    const callId = message.call.id;

    // Extract qualification signals
    const qualificationData = extractQualificationSignals(transcript);

    // Update Salesforce Lead in real-time
    try {
      await updateSalesforceLeadScore(callId, qualificationData);
    } catch (error) {
      console.error('Salesforce update failed:', error);
      // Don't block the call - log and continue
    }
  }

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

function extractQualificationSignals(transcript) {
  // Parse transcript for BANT criteria
  return {
    hasBudget: /budget|afford|spend/i.test(transcript),
    hasAuthority: /decision|approve|sign/i.test(transcript),
    hasNeed: /problem|challenge|need/i.test(transcript),
    hasTimeline: /when|timeline|urgency/i.test(transcript)
  };
}

async function updateSalesforceLeadScore(callId, signals) {
  const score = Object.values(signals).filter(Boolean).length * 25;

  const response = await fetch('https://yourinstance.salesforce.com/services/data/v58.0/sobjects/Lead/00Q...', {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${process.env.SALESFORCE_ACCESS_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      Lead_Score__c: score,
      Last_Call_ID__c: callId,
      Qualification_Status__c: score >= 75 ? 'Qualified' : 'Nurture'
    })
  });

  if (!response.ok) {
    throw new Error(`Salesforce API error: ${response.status}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Outbound Call Trigger from Salesforce

When a lead enters your Salesforce queue, trigger an outbound qualification call via Vapi's API.

async function initiateQualificationCall(leadPhone, leadId) {
  try {
    const response = await fetch('https://api.vapi.ai/call', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.VAPI_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        assistant: assistantConfig,
        phoneNumber: leadPhone,
        metadata: {
          salesforceLeadId: leadId,
          callPurpose: 'qualification'
        }
      })
    });

    if (!response.ok) {
      throw new Error(`Vapi API error: ${response.status}`);
    }

    const callData = await response.json();
    return callData.id;
  } catch (error) {
    console.error('Call initiation failed:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Error Handling & Edge Cases

Race condition: Salesforce API timeout during call. Solution: Queue updates asynchronously, don't block Vapi responses.

False qualification: Lead says "yes" to everything. Solution: Require 2+ specific details per BANT criterion, not just keywords.

Network jitter: Webhook receives events out of order. Solution: Add sequence numbers and reorder buffer.

Testing & Validation

Test with Salesforce sandbox. Verify lead score updates happen within 2 seconds of transcript events. Check that failed Salesforce writes don't crash the call.

Common Issues & Fixes

Issue: Duplicate lead records created.

Fix: Use Lead.Phone as external ID in upsert operations.

Issue: Stale OAuth tokens.

Fix: Implement token refresh 5 minutes before expiry, not on failure.

System Diagram

Audio processing pipeline from microphone input to speaker output.

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

    Input --> Buffer
    Buffer --> VAD
    VAD -->|Voice Detected| STT
    VAD -->|Silence| Error
    STT --> NLU
    NLU -->|Intent Recognized| API
    NLU -->|Intent Recognized| DB
    API --> LLM
    DB --> LLM
    LLM --> TTS
    TTS --> Output
    STT -->|Error| Error
    API -->|Error| Error
    DB -->|Error| Error
    Error --> Output
Enter fullscreen mode Exit fullscreen mode

Testing & Validation

Local Testing

Test the integration locally before deploying to production. Use ngrok to expose your webhook endpoint and validate the full call flow.

// Test webhook signature validation
const testWebhook = async () => {
  const testPayload = {
    message: {
      type: 'transcript',
      transcript: 'I need 50 licenses for enterprise deployment',
      role: 'user'
    },
    call: { id: 'test-call-123' }
  };

  const signature = crypto
    .createHmac('sha256', process.env.VAPI_SERVER_SECRET)
    .update(JSON.stringify(testPayload))
    .digest('hex');

  try {
    const response = await fetch('http://localhost:3000/webhook/vapi', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-vapi-signature': signature
      },
      body: JSON.stringify(testPayload)
    });

    if (!response.ok) throw new Error(`Webhook test failed: ${response.status}`);
    const result = await response.json();
    console.log('Qualification signals:', result);
  } catch (error) {
    console.error('Local test failed:', error);
  }
};
Enter fullscreen mode Exit fullscreen mode

This will bite you: Webhook signature validation fails if you modify the payload before hashing. Hash the RAW request body, not the parsed JSON.

Webhook Validation

Verify Salesforce updates trigger correctly by checking the Lead object after test calls. Query the AI_Qualification_Score__c field to confirm the score was written. If the score is null, check your Salesforce API credentials and field-level security settings—most failures happen because the integration user lacks write permissions on custom fields.

Real-World Example

Barge-In Scenario

A prospect interrupts the AI agent mid-pitch: "Wait, I'm not interested in enterprise plans." The agent must stop talking immediately, process the partial transcript, and pivot the conversation. Most implementations fail here because they don't flush the TTS buffer or handle overlapping audio streams.

// Handle barge-in with buffer flush and turn-taking logic
app.post('/webhook/vapi', async (req, res) => {
  const { message } = req.body;

  if (message.type === 'speech-update') {
    const { transcript, isFinal } = message;

    // Detect interruption from partial transcript
    if (!isFinal && transcript.toLowerCase().includes('wait')) {
      // Cancel ongoing TTS immediately
      await fetch(`https://api.vapi.ai/call/${message.call.id}/interrupt`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.VAPI_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ flushBuffer: true }) // Note: Endpoint inferred from standard API patterns
      });

      // Update conversation state to prevent race condition
      if (global.isProcessing) return res.status(200).send('OK');
      global.isProcessing = true;

      // Extract qualification signals from interruption
      const qualificationData = extractQualificationSignals(transcript);
      await updateSalesforceLeadScore(message.call.metadata.leadId, qualificationData);

      global.isProcessing = false;
    }
  }

  res.status(200).send('OK');
});
Enter fullscreen mode Exit fullscreen mode

Event Logs

Real webhook payload when prospect interrupts at 00:14 (agent was at word 23 of 45):

{
  "message": {
    "type": "speech-update",
    "transcript": "wait I'm not interested",
    "isFinal": false,
    "timestamp": "2024-01-15T10:23:14.234Z"
  },
  "call": {
    "id": "call_abc123",
    "status": "in-progress",
    "metadata": { "leadId": "00Q5e000001XYZ" }
  }
}
Enter fullscreen mode Exit fullscreen mode

The isFinal: false flag indicates partial STT output. Production systems must handle these partials to achieve sub-600ms interrupt detection. Waiting for isFinal: true adds 800-1200ms latency.

Edge Cases

Multiple rapid interrupts: Prospect says "wait... actually... hold on" in 2 seconds. Without the isProcessing guard, you'll trigger 3 concurrent Salesforce updates and corrupt the lead score. The lock prevents this race condition.

False positives: Background noise triggers VAD. Filter partials under 3 words: if (transcript.split(' ').length < 3) return; This cuts false interrupts by 70% in noisy environments.

Network timeout on Salesforce API: If updateSalesforceLeadScore() hangs, the webhook times out after 5 seconds and Vapi retries. Implement async processing: queue the update, return 200 immediately, process in background worker.

Common Issues & Fixes

Race Condition: Webhook Fires Before Salesforce Record Exists

Problem: Your voice agent calls updateSalesforceLeadScore() but the lead record hasn't synced yet from your CRM → 404 errors, failed qualification attempts.

Why this breaks: Salesforce API has 200-800ms replication lag across instances. If your webhook fires immediately after lead creation, the record isn't queryable yet.

// WRONG: Immediate webhook call fails
app.post('/webhook/vapi', async (req, res) => {
  const { callId, transcript } = req.body;
  const leadId = req.body.metadata?.salesforceLeadId;

  // This WILL fail if lead was just created
  await updateSalesforceLeadScore(leadId, score);
});

// FIX: Implement retry with exponential backoff
async function updateWithRetry(leadId, score, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(`https://yourinstance.salesforce.com/services/data/v58.0/sobjects/Lead/${leadId}`, {
        method: 'PATCH',
        headers: {
          'Authorization': `Bearer ${process.env.SALESFORCE_ACCESS_TOKEN}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ Qualification_Score__c: score })
      });

      if (response.status === 404 && i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 500)); // 500ms, 1s, 2s
        continue;
      }

      if (!response.ok) throw new Error(`Salesforce API error: ${response.status}`);
      return await response.json();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Transcript Truncation on Long Calls

Problem: Calls exceeding 8 minutes hit Vapi's default transcript buffer limit → extractQualificationSignals() only sees partial conversation, misses key objections.

Fix: Process transcripts incrementally using transcript.partial events instead of waiting for call.ended. Store conversation state in Redis with 15-minute TTL:

const sessions = new Map(); // Production: Use Redis with TTL

app.post('/webhook/vapi', (req, res) => {
  const { type, transcript, callId } = req.body;

  if (type === 'transcript.partial') {
    const session = sessions.get(callId) || { fullTranscript: '', signals: [] };
    session.fullTranscript += transcript + ' ';

    // Extract signals incrementally (every 30 seconds of speech)
    if (session.fullTranscript.length % 500 === 0) {
      const newSignals = extractQualificationSignals(session.fullTranscript);
      session.signals = [...session.signals, ...newSignals];
    }

    sessions.set(callId, session);
  }

  res.sendStatus(200);
});
Enter fullscreen mode Exit fullscreen mode

OAuth Token Expiration Mid-Call

Problem: Salesforce access tokens expire after 2 hours. If a qualification call starts at 1:58 into token lifetime, the updateSalesforceLeadScore() call at 2:03 fails with 401.

Fix: Implement token refresh 5 minutes before expiration. Store token metadata with expiry timestamp:

let tokenCache = {
  accessToken: null,
  expiresAt: null
};

async function getSalesforceToken() {
  const now = Date.now();

  // Refresh if token expires in < 5 minutes
  if (!tokenCache.accessToken || tokenCache.expiresAt - now < 300000) {
    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.SALESFORCE_CLIENT_ID,
        client_secret: process.env.SALESFORCE_CLIENT_SECRET,
        refresh_token: process.env.SALESFORCE_REFRESH_TOKEN
      })
    });

    const data = await response.json();
    tokenCache = {
      accessToken: data.access_token,
      expiresAt: now + (data.expires_in * 1000)
    };
  }

  return tokenCache.accessToken;
}
Enter fullscreen mode Exit fullscreen mode

Complete Working Example

Most Salesforce voice integrations fail in production because they treat OAuth tokens as static and don't handle webhook signature validation. Here's the full server that solves both problems.

Full Server Code

This is the complete production-ready implementation. Copy-paste this entire file and run it. It includes OAuth token refresh, webhook signature validation, and real-time lead scoring.

// server.js - Complete Salesforce Voice AI Integration
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Token cache with automatic refresh
const tokenCache = {
  accessToken: null,
  expiresAt: 0
};

// Get or refresh Salesforce OAuth token
async function getSalesforceToken() {
  const now = Date.now();
  if (tokenCache.accessToken && tokenCache.expiresAt > now) {
    return tokenCache.accessToken;
  }

  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: 'client_credentials',
        client_id: process.env.SALESFORCE_CLIENT_ID,
        client_secret: process.env.SALESFORCE_CLIENT_SECRET
      })
    });

    if (!response.ok) throw new Error(`OAuth failed: ${response.status}`);

    const data = await response.json();
    tokenCache.accessToken = data.access_token;
    tokenCache.expiresAt = now + (data.expires_in * 1000) - 60000; // Refresh 1min early

    return data.access_token;
  } catch (error) {
    console.error('Token refresh failed:', error);
    throw error;
  }
}

// Extract qualification signals from conversation
function extractQualificationSignals(transcript) {
  const signals = {
    budget: /\$[\d,]+|budget.*\d+/.test(transcript),
    timeline: /next (week|month|quarter)|asap|urgent/.test(transcript),
    authority: /decision maker|ceo|vp|director/.test(transcript),
    need: /problem|challenge|need|looking for/.test(transcript)
  };

  const score = Object.values(signals).filter(Boolean).length * 25;
  return { signals, score };
}

// Update Salesforce lead with retry logic
async function updateWithRetry(leadId, qualificationData, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const accessToken = await getSalesforceToken();
      const response = await fetch(
        `https://${process.env.SALESFORCE_INSTANCE}.salesforce.com/services/data/v58.0/sobjects/Lead/${leadId}`,
        {
          method: 'PATCH',
          headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            Lead_Score__c: qualificationData.score,
            Qualification_Signals__c: JSON.stringify(qualificationData.signals),
            Last_AI_Call_Date__c: new Date().toISOString()
          })
        }
      );

      if (response.status === 401 && i < retries - 1) {
        tokenCache.accessToken = null; // Force token refresh
        continue;
      }

      if (!response.ok) throw new Error(`Salesforce update failed: ${response.status}`);
      return await response.json();
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
    }
  }
}

// Webhook handler with signature validation
app.post('/webhook/vapi', async (req, res) => {
  // Validate webhook signature
  const signature = req.headers['x-vapi-signature'];
  const expectedSig = crypto
    .createHmac('sha256', process.env.VAPI_SERVER_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (signature !== expectedSig) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const { type, call, message } = req.body;

  // Handle call completion
  if (type === 'end-of-call-report') {
    const transcript = message?.transcript || '';
    const leadId = call?.metadata?.leadId;

    if (!leadId) {
      console.error('Missing leadId in call metadata');
      return res.status(400).json({ error: 'Missing leadId' });
    }

    try {
      const qualificationData = extractQualificationSignals(transcript);
      await updateWithRetry(leadId, qualificationData);

      console.log(`Lead ${leadId} scored: ${qualificationData.score}/100`);
      return res.json({ success: true, score: qualificationData.score });
    } catch (error) {
      console.error('Webhook processing failed:', error);
      return res.status(500).json({ error: 'Processing failed' });
    }
  }

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

// Initiate outbound qualification call
app.post('/api/qualify-lead', async (req, res) => {
  const { leadId, phoneNumber } = req.body;

  if (!leadId || !phoneNumber) {
    return res.status(400).json({ error: 'Missing leadId or phoneNumber' });
  }

  try {
    const response = await fetch('https://api.vapi.ai/call', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.VAPI_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        assistantId: process.env.VAPI_ASSISTANT_ID,
        customer: { number: phoneNumber },
        metadata: { leadId }
      })
    });

    if (!response.ok) throw new Error(`VAPI call failed: ${response.status}`);

    const callData = await response.json();
    res.json({ success: true, callId: callData.id });
  } catch (error) {
    console.error('Call initiation failed:', error);
    res.status(500).json({ error: 'Failed to initiate call' });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

Run Instructions

Environment variables (create .env file):

VAPI_API_KEY=your_vapi_key
VAPI_ASSISTANT_ID=your_assistant_id
VAPI_SERVER_SECRET=your_webhook_secret
SALESFORCE_CLIENT_ID=your_salesforce_client_id
SALESFORCE_CLIENT_SECRET=your_salesforce_client_secret
SALESFORCE_INSTANCE=your_instance.my.salesforce.com
PORT=3000
Enter fullscreen mode Exit fullscreen mode

Install dependencies:

npm install express node-fetch
Enter fullscreen mode Exit fullscreen mode

Start server:

node server.js
Enter fullscreen mode Exit fullscreen mode

Test webhook locally (use ngrok for public URL):

ngrok http 3000
# Configure webhook URL in VAPI dashboard: https://your-ngrok-url.ngrok.io/webhook/vapi
Enter fullscreen mode Exit fullscreen mode

Trigger qualification call:

curl -X POST http://localhost:3000/api/qualify-lead \
  -H "Content-Type: application/json" \
  -d '{"leadId":"00Q5e00000ABC123","phoneNumber":"+15551234567"}'
Enter fullscreen mode Exit fullscreen mode

The server handles token refresh automatically, validates all webhook signatures, and ret

FAQ

How does Voice AI integrate with Salesforce's REST API for real-time lead updates?

Voice AI connects to Salesforce via OAuth 2.0 authentication and REST API calls. During a call, the AI extracts qualification signals (budget, timeline, authority) from the conversation transcript. These signals trigger immediate API requests to Salesforce's /services/data/v58.0/sobjects/Lead/{leadId} endpoint to update lead scores and custom fields. The integration uses webhook callbacks to process transcripts asynchronously—when VAPI sends a transcript event, your server parses the conversation, calculates a qualification score, and pushes updates to Salesforce within 200-400ms. This eliminates manual data entry and ensures sales teams see real-time lead intelligence in their CRM dashboards.

What latency should I expect between call completion and Salesforce lead score updates?

End-to-end latency from call end to Salesforce update typically ranges 800ms-2s. Breakdown: VAPI webhook delivery (100-200ms), transcript processing + signal extraction (200-400ms), Salesforce API call (300-600ms), retry logic if rate-limited (adds 1-2s). Network jitter on mobile calls can add 100-300ms. To minimize delays, cache Salesforce access tokens (avoid OAuth handshake per request), use connection pooling for API calls, and process webhooks asynchronously. If Salesforce returns HTTP 429 (rate limit exceeded), implement exponential backoff with 3 retries max.

How does this compare to native Salesforce Einstein Voice or Twilio Voice integrations?

Salesforce Einstein Voice requires Enterprise+ licenses ($300+/user/month) and lacks customizable conversation flows—it's designed for internal sales calls, not outbound lead qualification. Twilio Voice handles telephony but requires you to build the entire AI layer (STT, NLP, TTS) separately. VAPI + Salesforce gives you pre-built conversational AI with function calling to trigger Salesforce updates mid-call, at $0.05-0.10/min vs Einstein's $3-5/call equivalent. You control the AI logic, conversation branching, and qualification criteria without vendor lock-in.

What happens if the Salesforce API call fails during a live call?

Implement a retry queue with exponential backoff. If the initial API request fails (network timeout, 503 error), queue the update in Redis or a database with the callId and retry after 2s, 4s, 8s (max 3 attempts). Log failures to a dead-letter queue for manual review. The AI call continues uninterrupted—qualification signals are stored in session state and pushed to Salesforce post-call if real-time updates fail. Never block the call flow waiting for Salesforce responses. Use async processing to decouple voice interaction from CRM writes.

Can I qualify leads in languages other than English?

Yes. VAPI supports 30+ languages via Deepgram (STT) and ElevenLabs (TTS). Set transcriber.language to the target locale (e.g., es for Spanish, fr for French). Your qualification logic must handle multilingual transcripts—use language-specific keyword matching or a multilingual NLP model (OpenAI GPT-4 supports 50+ languages). Salesforce field updates remain language-agnostic (you're writing scores/booleans, not raw text). Latency increases 50-100ms for non-English due to smaller STT model training sets.

Resources

Official Documentation:

GitHub Repositories:

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/workflows/quickstart
  6. https://docs.vapi.ai/observability/evals-quickstart
  7. https://docs.vapi.ai/assistants/structured-outputs-quickstart

Top comments (0)