Advertisement
Table of Contents
Integrate Voice AI with Salesforce for Sales Automation: A Real Developer's Guide
TL;DR
Most voice agents fail when syncing with Salesforce—OAuth tokens expire mid-call, CRM queries timeout, and call transcripts never reach your database. Here's what works: VAPI handles the conversation, Twilio routes inbound calls, your server bridges both via OAuth 2.0 to query Salesforce APIs in real-time. Result: automated case creation, live contact lookups, and call recordings attached to opportunities—no manual data entry.
Prerequisites
API Keys & Credentials
You'll need a VAPI API key (grab it from your dashboard at vapi.ai). Generate a Salesforce OAuth token via your org's Connected App settings—you'll use this for real-time Salesforce API queries. If using Twilio for phone integration, grab your Account SID and Auth Token from console.twilio.com.
System & SDK Requirements
Node.js 16+ (for async/await and fetch support). Install axios or use native fetch for HTTP calls. Salesforce API v57.0 or later (REST API for case management and contact queries). VAPI SDK is optional—raw HTTP calls work fine for production.
Network & Security
A publicly accessible webhook endpoint (ngrok for local testing, or deployed server for production). HTTPS required for Salesforce OAuth and webhook signature validation. Store all credentials in .env files—never hardcode API keys.
Salesforce Org Setup
Enable API access in your org. Create a Connected App with OAuth scopes: api, refresh_token, offline_access. Ensure your user has permissions for case creation and contact updates.
Twilio: Get Twilio Voice API → Get Twilio
Step-by-Step Tutorial
Configuration & Setup
Most Salesforce voice integrations fail because developers treat OAuth as an afterthought. You need three active sessions: Vapi's WebSocket, Salesforce's OAuth token, and Twilio's call state. If any expires mid-call, your agent goes silent.
Required credentials:
- Vapi API key (Dashboard → Settings)
- Salesforce Connected App (OAuth 2.0 with refresh tokens)
- Twilio Account SID + Auth Token
- ngrok or production domain for webhooks
// Production-grade environment config
const config = {
vapi: {
apiKey: process.env.VAPI_API_KEY,
webhookSecret: process.env.VAPI_WEBHOOK_SECRET
},
salesforce: {
clientId: process.env.SF_CLIENT_ID,
clientSecret: process.env.SF_CLIENT_SECRET,
redirectUri: process.env.SF_REDIRECT_URI,
instanceUrl: process.env.SF_INSTANCE_URL // e.g., https://yourorg.my.salesforce.com
},
twilio: {
accountSid: process.env.TWILIO_ACCOUNT_SID,
authToken: process.env.TWILIO_AUTH_TOKEN,
phoneNumber: process.env.TWILIO_PHONE_NUMBER
}
};
// Session store with 30-minute TTL
const sessions = new Map();
const SESSION_TTL = 30 * 60 * 1000;
Critical: Salesforce tokens expire after 2 hours. Implement refresh logic BEFORE building the voice flow, or you'll debug token failures during live calls.
Architecture & Flow
flowchart LR
A[User Call] --> B[Twilio]
B --> C[Vapi Assistant]
C --> D[Function Call: querySalesforce]
D --> E[Your Server /webhook]
E --> F[Salesforce API]
F --> E
E --> C
C --> A
The integration layer is YOUR server, not Vapi or Twilio. Vapi triggers function calls, your server executes Salesforce queries, then returns structured data. This separation prevents race conditions where the LLM tries to parse raw API responses.
Step-by-Step Implementation
1. Salesforce OAuth Flow
// OAuth token exchange - run this ONCE per user
async function getSalesforceToken(authCode) {
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: 'authorization_code',
code: authCode,
client_id: config.salesforce.clientId,
client_secret: config.salesforce.clientSecret,
redirect_uri: config.salesforce.redirectUri
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Salesforce OAuth failed: ${error.error_description}`);
}
const tokens = await response.json();
// Store tokens.access_token and tokens.refresh_token securely
return tokens;
} catch (error) {
console.error('OAuth Error:', error);
throw error;
}
}
2. Vapi Assistant with Salesforce Function
Configure your assistant with a custom tool that queries Salesforce. The LLM decides WHEN to call this function based on conversation context.
const assistantConfig = {
model: {
provider: "openai",
model: "gpt-4",
messages: [{
role: "system",
content: "You are a sales assistant. When users ask about leads, opportunities, or cases, use the querySalesforce function. Always confirm the data type before querying."
}]
},
voice: {
provider: "11labs",
voiceId: "21m00Tcm4TlvDq8ikWAM"
},
transcriber: {
provider: "deepgram",
model: "nova-2",
language: "en"
},
functions: [{
name: "querySalesforce",
description: "Query Salesforce for leads, opportunities, or cases. Returns structured data.",
parameters: {
type: "object",
properties: {
objectType: {
type: "string",
enum: ["Lead", "Opportunity", "Case"],
description: "Salesforce object to query"
},
filters: {
type: "object",
description: "SOQL WHERE clause filters (e.g., {Status: 'Open', Priority: 'High'})"
}
},
required: ["objectType"]
},
async: true // Critical: Salesforce queries take 200-800ms
}]
};
3. Webhook Handler for Function Execution
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Webhook signature validation - prevents replay attacks
function validateWebhook(req) {
const signature = req.headers['x-vapi-signature'];
const payload = JSON.stringify(req.body);
const hash = crypto
.createHmac('sha256', config.vapi.webhookSecret)
.update(payload)
.digest('hex');
return signature === hash;
}
app.post('/webhook/vapi', async (req, res) => {
if (!validateWebhook(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { message } = req.body;
// Handle function call from Vapi
if (message.type === 'function-call' && message.functionCall.name === 'querySalesforce') {
const { objectType, filters } = message.functionCall.parameters;
const callId = message.call.id;
try {
// Get session token (implement token refresh here)
const session = sessions.get(callId);
if (!session || Date.now() > session.expiresAt) {
throw new Error('Session expired - refresh token required');
}
// Build SOQL query
let whereClause = '';
if (filters) {
whereClause = 'WHERE ' + Object.entries(filters)
.map(([key, val]) => `${key} = '${val}'`)
.join(' AND ');
}
const soql = `SELECT Id, Name, Status FROM ${objectType} ${whereClause} LIMIT 10`;
// Query Salesforce
const sfResponse = await fetch(
`${config.salesforce.instanceUrl}/services/data/v58.0/query?q=${encodeURIComponent(soql)}`,
{
headers: {
'Authorization': `Bearer ${session.accessToken}`,
'Content-Type': 'application/json'
}
}
);
if (!sfResponse.ok) {
throw new Error(`Salesforce API error: ${sfResponse.status}`);
}
const data = await sfResponse.json();
// Return structured response to Vapi
return res.json({
result: {
records: data.records,
totalSize: data.totalSize
}
});
} catch (error) {
console.error('Salesforce Query Error:', error);
return res.json({
error: error.message,
fallback: "I'm having trouble accessing Salesforce right now. Let me transfer you to a human agent."
});
}
}
res.sendStatus(200);
});
app.listen(3000, () => console.log('Webhook server running on port 3000'));
Error Handling & Edge Cases
Token expiration mid-call: Implement refresh token logic in your
System Diagram
Audio processing pipeline from microphone input to speaker output.
graph LR
Mic[Microphone Input]
ABuffer[Audio Buffering]
VAD[Voice Activity Detection]
STT[Speech-to-Text Engine]
NLU[Intent Recognition]
LLM[Response Generation]
TTS[Text-to-Speech Engine]
Speaker[Speaker Output]
Error[Error Handling]
Retry[Retry Mechanism]
Mic --> ABuffer
ABuffer --> VAD
VAD -->|Detected| STT
VAD -->|Not Detected| Error
STT -->|Success| NLU
STT -->|Failure| Error
NLU --> LLM
LLM --> TTS
TTS --> Speaker
Error --> Retry
Retry --> ABuffer
Testing & Validation
Local Testing with ngrok
Most Voice AI Salesforce integrations break because webhooks fail silently. Expose your local server:
ngrok http 3000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
Update your webhook endpoint in the Vapi dashboard to https://abc123.ngrok.io/webhook/vapi. Test the full flow:
// Test webhook signature validation locally
const testPayload = JSON.stringify({
message: { type: 'function-call', functionCall: { name: 'querySalesforce' } }
});
const testSignature = crypto
.createHmac('sha256', process.env.VAPI_SERVER_SECRET)
.update(testPayload)
.digest('hex');
console.log('Expected signature:', testSignature);
// Compare with x-vapi-signature header in ngrok inspector
Real-world problem: Signature mismatches cause 401 errors. The ngrok inspector at http://localhost:4040 shows raw headers—verify x-vapi-signature matches your computed hash.
Webhook Validation
Validate Salesforce OAuth tokens before production. Call the Salesforce API directly:
// Verify token works outside webhook context
const tokens = await getSalesforceToken();
const testResponse = await fetch(`${tokens.instance_url}/services/data/v58.0/sobjects/Case`, {
headers: { 'Authorization': `Bearer ${tokens.access_token}` }
});
if (!testResponse.ok) {
throw new Error(`Salesforce auth failed: ${testResponse.status}`);
}
console.log('âś“ Salesforce connection validated');
What breaks in production: Expired refresh tokens return 401 after 2 hours. Test token refresh logic by manually expiring access_token in your session store.
Real-World Example
Barge-In Scenario
Production voice agents break when users interrupt mid-sentence. Here's what actually happens when a prospect cuts off your Salesforce-integrated agent:
// Streaming STT handler with barge-in detection
let isProcessing = false;
let currentAudioBuffer = [];
app.post('/webhook/vapi', async (req, res) => {
const { type, call, transcript } = req.body;
if (type === 'transcript' && transcript.partial) {
// User started speaking - cancel TTS immediately
if (isProcessing) {
currentAudioBuffer = []; // Flush buffer to prevent old audio
isProcessing = false;
console.log(`[${new Date().toISOString()}] Barge-in detected, buffer flushed`);
}
}
if (type === 'function-call' && req.body.functionCall.name === 'querySalesforce') {
if (isProcessing) return res.json({ result: 'Processing previous request' });
isProcessing = true;
try {
const tokens = await getSalesforceToken();
const { objectType, filters } = req.body.functionCall.parameters;
const whereClause = filters.Status ? `WHERE Status = '${filters.Status}'` : '';
const soql = `SELECT Id, Subject, Priority FROM ${objectType} ${whereClause} LIMIT 5`;
const sfResponse = await fetch(
`${config.salesforce.instanceUrl}/services/data/v57.0/query?q=${encodeURIComponent(soql)}`,
{
headers: { 'Authorization': `Bearer ${tokens.access_token}` }
}
);
const data = await sfResponse.json();
isProcessing = false;
return res.json({
result: `Found ${data.totalSize} cases: ${data.records.map(r => r.Subject).join(', ')}`
});
} catch (error) {
isProcessing = false;
console.error(`[${new Date().toISOString()}] SF query failed:`, error.message);
return res.json({ error: 'Salesforce temporarily unavailable' });
}
}
res.sendStatus(200);
});
Event Logs
Timestamp: 14:23:41.203 - User: "Show me high priority ca—"
Timestamp: 14:23:41.287 - transcript.partial event fires (84ms latency)
Timestamp: 14:23:41.291 - Buffer flushed, isProcessing reset
Timestamp: 14:23:42.105 - User completes: "cases from today"
Timestamp: 14:23:42.198 - function-call event triggers Salesforce query
Timestamp: 14:23:42.673 - SF API returns (475ms round-trip)
Edge Cases
Multiple rapid interruptions: If user interrupts twice within 200ms, the race condition guard (if (isProcessing) return) prevents duplicate Salesforce API calls. Without this, you'll hit rate limits (5 concurrent requests max on Enterprise).
False positives from background noise: Default VAD threshold (0.3) triggers on keyboard clicks during screenshares. Increase to 0.5 in transcriber.endpointing config or you'll flush buffers on non-speech audio, causing choppy responses.
OAuth token expiration mid-call: The getSalesforceToken() function doesn't cache tokens. Production systems need a 55-minute TTL cache (tokens expire at 60min) to avoid 401 errors during long calls.
Common Issues & Fixes
Race Conditions in Concurrent Salesforce Queries
Most Voice AI + Salesforce integrations break when multiple function calls fire simultaneously. VAPI's LLM can trigger 2-3 Salesforce queries in parallel (e.g., "Check case status AND pull contact details"), but Salesforce's OAuth token refresh isn't thread-safe. You'll hit 401 Unauthorized errors mid-call when tokens expire during concurrent requests.
Fix: Implement a token refresh lock with request queuing:
let tokenRefreshPromise = null;
async function getSalesforceToken() {
// Reuse in-flight refresh to prevent race condition
if (tokenRefreshPromise) return tokenRefreshPromise;
tokenRefreshPromise = (async () => {
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 tokens = await response.json();
return tokens.access_token;
} finally {
tokenRefreshPromise = null; // Reset lock after completion
}
})();
return tokenRefreshPromise;
}
This pattern prevents duplicate token refreshes when 3+ function calls execute within the same 200ms window. Without it, you'll burn through Salesforce API limits (5,000 calls/day on Developer Edition) and trigger rate limit errors (REQUEST_LIMIT_EXCEEDED).
Webhook Signature Validation Failures
VAPI's webhook signatures fail validation when your server uses body-parsing middleware that mutates req.body. Express's express.json() converts the raw buffer to an object, breaking HMAC-SHA256 verification. You'll see 403 Forbidden responses even with correct secrets.
Fix: Capture raw body BEFORE parsing:
app.post('/webhook/vapi',
express.raw({ type: 'application/json' }), // Preserve raw buffer
(req, res) => {
const signature = req.headers['x-vapi-signature'];
const payload = req.body.toString('utf8'); // Convert buffer to string
const hash = crypto
.createHmac('sha256', process.env.VAPI_SERVER_SECRET)
.update(payload)
.digest('hex');
if (hash !== signature) {
return res.status(403).json({ error: 'Invalid signature' });
}
const data = JSON.parse(payload); // Parse AFTER validation
// Process webhook...
res.json({ success: true });
}
);
Salesforce SOQL Injection via Voice Input
User speech like "Show cases for contact name equals admin' OR '1'='1" will inject malicious SOQL if you concatenate strings directly. Salesforce doesn't auto-escape single quotes in dynamic queries, leading to unauthorized data access.
Fix: Use parameterized WHERE clauses with explicit escaping:
// NEVER do this (vulnerable):
// const soql = `SELECT Id FROM Case WHERE ContactId = '${contactId}'`;
// DO THIS (safe):
const whereClause = filters
.map(f => `${f.field} = '${f.value.replace(/'/g, "\\'")}'`) // Escape quotes
.join(' AND ');
const soql = `SELECT Id, Status, Priority FROM Case WHERE ${whereClause} LIMIT 10`;
Salesforce's REST API doesn't support prepared statements, so manual escaping is mandatory. This prevents MALFORMED_QUERY errors and data leaks when users say punctuation-heavy names.
Complete Working Example
Here's the full production server that handles OAuth, webhooks, and Salesforce queries. This is the proof the tutorial works—copy-paste this into server.js and you're live.
Full Server Code
// server.js - Complete Vapi + Salesforce integration
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Configuration from environment
const config = {
vapi: {
apiKey: process.env.VAPI_API_KEY,
serverUrlSecret: process.env.VAPI_SERVER_URL_SECRET
},
salesforce: {
clientId: process.env.SF_CLIENT_ID,
clientSecret: process.env.SF_CLIENT_SECRET,
redirectUri: process.env.SF_REDIRECT_URI,
instanceUrl: process.env.SF_INSTANCE_URL
}
};
// Session storage with TTL
const sessions = {};
const SESSION_TTL = 3600000; // 1 hour
// Token refresh guard to prevent race conditions
let tokenRefreshPromise = null;
// OAuth: Redirect user to Salesforce login
app.get('/oauth/login', (req, res) => {
const authUrl = `https://login.salesforce.com/services/oauth2/authorize?` +
`response_type=code&client_id=${config.salesforce.clientId}&` +
`redirect_uri=${encodeURIComponent(config.salesforce.redirectUri)}`;
res.redirect(authUrl);
});
// OAuth: Handle callback and exchange code for tokens
app.get('/oauth/callback', async (req, res) => {
const { code } = req.query;
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: 'authorization_code',
code: code,
client_id: config.salesforce.clientId,
client_secret: config.salesforce.clientSecret,
redirect_uri: config.salesforce.redirectUri
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`OAuth failed: ${error}`);
}
const tokens = await response.json();
// Store tokens with expiration
const sessionId = crypto.randomBytes(16).toString('hex');
sessions[sessionId] = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
instanceUrl: tokens.instance_url,
expiresAt: Date.now() + SESSION_TTL
};
// Cleanup expired sessions
setTimeout(() => delete sessions[sessionId], SESSION_TTL);
res.send(`<html><body><h1>Connected to Salesforce</h1><p>Session ID: ${sessionId}</p></body></html>`);
} catch (error) {
console.error('OAuth Error:', error);
res.status(500).send('Authentication failed');
}
});
// Token refresh with race condition guard
async function getSalesforceToken(sessionId) {
const session = sessions[sessionId];
if (!session) throw new Error('Session expired');
// Return cached token if still valid (5min buffer)
if (session.expiresAt > Date.now() + 300000) {
return session.accessToken;
}
// Guard against concurrent refresh attempts
if (tokenRefreshPromise) return tokenRefreshPromise;
tokenRefreshPromise = (async () => {
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',
refresh_token: session.refreshToken,
client_id: config.salesforce.clientId,
client_secret: config.salesforce.clientSecret
})
});
if (!response.ok) throw new Error(`Token refresh failed: ${response.status}`);
const tokens = await response.json();
session.accessToken = tokens.access_token;
session.expiresAt = Date.now() + SESSION_TTL;
return tokens.access_token;
} finally {
tokenRefreshPromise = null; // Release lock
}
})();
return tokenRefreshPromise;
}
// Webhook signature validation
function validateWebhook(payload, signature) {
const hash = crypto
.createHmac('sha256', config.vapi.serverUrlSecret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hash));
}
// Main webhook handler
app.post('/webhook/vapi', async (req, res) => {
const payload = req.body;
const signature = req.headers['x-vapi-signature'];
// Security: Validate webhook signature
if (!validateWebhook(payload, signature)) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Unauthorized' });
}
// Handle function calls from Vapi
if (payload.message?.type === 'function-call') {
const { functionCall, call } = payload.message;
const sessionId = call.metadata?.sessionId;
try {
const accessToken = await getSalesforceToken(sessionId);
// Build SOQL query from function parameters
const { objectType, filters } = functionCall.parameters;
let whereClause = '';
if (filters?.Status) {
whereClause = `WHERE Status = '${filters.Status}'`;
}
if (filters?.Priority) {
whereClause += whereClause ? ` AND Priority = '${filters.Priority}'` : `WHERE Priority = '${filters.Priority}'`;
}
const soql = `SELECT Id, Subject, Status, Priority FROM ${objectType} ${whereClause} LIMIT 10`;
// Execute Salesforce query
const sfResponse = await fetch(
`${config.salesforce.instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(soql)}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
if (!sfResponse.ok) {
throw new Error(`Salesforce API error: ${sfResponse.status}`);
}
const data = await sfResponse.json();
// Return results to Vapi
return res.json({
result: {
records: data.records,
totalSize: data.totalSize
}
});
} catch (error) {
console.error('Function call error:', error);
return res.json({
error: error.message,
fallback: 'I encountered an error querying Salesforce. Please try again.'
});
}
}
// Acknowledge other webhook events
res.status(200).json({ received: true });
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'healthy', sessions: Object.keys(sessions).length });
});
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
1. Install dependencies:
## FAQ
### Technical Questions
**How does Vapi authenticate with Salesforce OAuth without exposing credentials?**
Vapi never touches your Salesforce credentials directly. Instead, implement a proxy server that handles OAuth token exchange. When Vapi's function calling triggers a Salesforce query, it hits YOUR endpoint (e.g., `/api/salesforce/query`), which uses `getSalesforceToken()` to fetch a fresh access token from Salesforce's OAuth endpoint. Store the `refreshToken` server-side in encrypted storage—never in the Vapi config. The token refresh happens via `grant_type: "refresh_token"` before each API call, ensuring expired tokens don't break mid-conversation.
**What happens if Salesforce API returns an error during a live call?**
Vapi continues the conversation using the `fallback` response defined in your function schema. If a SOQL query fails (e.g., invalid `whereClause`), your server catches the error and returns `{ error: "Unable to fetch records", fallback: "Let me try a different search" }`. The voice agent hears the fallback message and can ask clarifying questions. Critical: set a timeout on Salesforce API calls (typically 3-5 seconds). If `sfResponse` doesn't arrive in time, return a fallback immediately rather than leaving the caller in silence.
**Can I query multiple Salesforce objects in a single call?**
Yes, but structure it carefully. Define separate functions for each object type (Accounts, Opportunities, Cases). When the voice agent needs data, it calls the appropriate function based on context. For example, if discussing a customer issue, it calls the Cases function with `objectType: "Case"`. Avoid chaining queries in one function—it increases latency and makes error handling messy. Each function should handle one object type and return structured data that the LLM can reason about.
### Performance
**Why is there latency between the user's request and Salesforce data appearing in the conversation?**
Three bottlenecks: (1) STT processing (200-400ms), (2) LLM reasoning to generate the function call (300-800ms), (3) Salesforce API response (500-2000ms depending on query complexity). Total: 1-4 seconds. Optimize by pre-warming connections—use connection pooling for Salesforce OAuth tokens so `getSalesforceToken()` returns cached tokens when possible. Also, simplify SOQL queries: avoid complex JOINs or large result sets. Request only the fields you need (`SELECT Id, Name, Amount FROM Opportunity WHERE...`), not `SELECT *`.
**How do I prevent webhook timeouts when Salesforce is slow?**
Vapi webhooks timeout after ~5 seconds. If your Salesforce query takes longer, implement async processing: your server immediately returns `{ status: "processing" }` and stores the `callId` in a queue. Once Salesforce responds, use Vapi's callback mechanism to inject the data back into the conversation. Alternatively, increase the timeout by wrapping the Salesforce call in a Promise with explicit error handling—if it exceeds 4 seconds, return a fallback and queue the data for later.
### Platform Comparison
**Should I use Vapi or Twilio for Salesforce voice integration?**
Vapi handles the AI/LLM layer and function calling natively—it's built for this. Twilio handles the telephony (inbound/outbound calls, call routing). Use both: Vapi for intelligence, Twilio for call infrastructure. Vapi can initiate calls via Twilio's API, or Twilio can route inbound calls to Vapi. Don't try to build LLM logic in Twilio—it's a telephony platform, not an AI platform. Vapi's function calling integrates directly with Salesforce; Twilio requires you to build that bridge yourself.
**Can I use Vapi's native voice without Twilio?**
Yes. Vapi can make outbound calls directly using its own infrastructure (no Twilio needed). However, if you need inbound call handling, call recording, or advanced routing, Twilio is still valuable. For Salesforce automation, the choice depends on your use case: outbound sales calls → Vapi alone. Inbound support calls with Salesforce case creation → Vapi + Twilio for routing.
## Resources
**VAPI**: Get Started with VAPI → [https://vapi.ai/?aff=misal](https://vapi.ai/?aff=misal)
**VAPI Documentation**
- [Official VAPI API Reference](https://docs.vapi.ai) – Complete endpoint specs, assistant configuration, function calling, webhook events
- [VAPI GitHub Repository](https://github.com/VapiAI/server-sdk-js) – Node.js SDK, example integrations, webhook validation patterns
**Salesforce Integration**
- [Salesforce OAuth 2.0 Authentication](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_understanding_oauth.htm) – Token refresh, scope management, security best practices
- [Salesforce REST API Reference](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_what_is_rest_api.htm) – SOQL queries, case/lead creation, real-time data updates
**Twilio Voice Integration**
- [Twilio Voice API Docs](https://www.twilio.com/docs/voice/api) – Call handling, webhook configuration, SIP integration with VAPI
**Production Deployment**
- [ngrok Documentation](https://ngrok.com/docs) – Secure webhook tunneling for local testing
- [Node.js Best Practices](https://nodejs.org/en/docs/guides/nodejs-performance-best-practices/) – Session management, memory optimization, error handling
## 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/chat/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.



