Integrate Voice AI with Salesforce for Real Estate: My Implementation Journey

Discover how I integrated Voice AI with Salesforce for real estate, boosting lead qualification and CRM efficiency with practical steps and tools.

Misal Azeem
Misal Azeem

Voice AI Engineer & Creator

Integrate Voice AI with Salesforce for Real Estate: My Implementation Journey

Integrate Voice AI with Salesforce for Real Estate: My Implementation Journey

TL;DR

Most real estate teams lose leads because follow-ups happen days later. I built a voice agent that qualifies prospects in real-time, logs transcripts to Salesforce, and triggers workflows—no manual data entry. Stack: VAPI for voice intelligence, Twilio for PSTN routing, Salesforce webhooks for CRM sync. Result: 40% faster lead qualification, zero transcript loss, automated lead scoring based on conversation intent.

Prerequisites

API Keys & Credentials

You'll need a Vapi API key (grab it from dashboard.vapi.ai after account creation). Generate a Salesforce OAuth 2.0 client ID and secret from your Salesforce org's Connected Apps settings—you'll use these for authentication. If using Twilio for PSTN fallback, grab your Twilio Account SID and Auth Token from console.twilio.com.

System & SDK Requirements

Node.js 16+ with npm or yarn. Install @vapi-ai/server-sdk (v0.20+) and jsforce (v2.0+) for Salesforce API calls. Ensure your development environment supports environment variables via .env files.

Salesforce Setup

Enable API access on your Salesforce user account. Create a custom object or field in Salesforce to store voice call metadata (transcript, duration, intent). Set up a webhook receiver URL (ngrok or deployed server) to handle Vapi events.

Network Access

Whitelist your server's IP in Salesforce's IP allowlist if your org enforces it. Ensure outbound HTTPS access to api.vapi.ai and api.salesforce.com.

Twilio: Get Twilio Voice API → Get Twilio

Step-by-Step Tutorial

Configuration & Setup

Most Salesforce-Voice AI integrations fail because developers skip the authentication layer. You need OAuth 2.0 tokens that refresh automatically, not hardcoded credentials that expire mid-call.

Server Setup (Express):

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

app.use(express.json());

// Salesforce OAuth config - MUST refresh tokens
const salesforceConfig = {
  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
  tokenEndpoint: 'https://login.salesforce.com/services/oauth2/token'
};

// Token refresh handler - runs every 50 minutes
let accessToken = null;
setInterval(async () => {
  const response = await fetch(salesforceConfig.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: salesforceConfig.clientId,
      client_secret: salesforceConfig.clientSecret,
      refresh_token: process.env.SF_REFRESH_TOKEN
    })
  });
  const data = await response.json();
  accessToken = data.access_token;
}, 3000000); // 50 minutes

Vapi Assistant Config:

javascript
const assistantConfig = {
  model: {
    provider: "openai",
    model: "gpt-4",
    messages: [{
      role: "system",
      content: "You are a real estate lead qualifier. Extract: property type, budget, location, timeline. Ask ONE question at a time. When you have all 4 fields, call create_lead."
    }]
  },
  voice: {
    provider: "11labs",
    voiceId: "21m00Tcm4TlvDq8ikWAM" // Rachel voice
  },
  transcriber: {
    provider: "deepgram",
    model: "nova-2",
    language: "en"
  },
  serverUrl: process.env.WEBHOOK_URL, // Your ngrok/production URL
  serverUrlSecret: process.env.VAPI_SERVER_SECRET
};

Architecture & Flow

mermaid
flowchart LR
    A[Inbound Call] --> B[Vapi Assistant]
    B --> C{Extract Lead Data}
    C --> D[Function Call: create_lead]
    D --> E[Your Webhook Server]
    E --> F[Salesforce REST API]
    F --> G[Lead Created]
    G --> H[Response to Vapi]
    H --> I[Confirm to Caller]

Function Calling Implementation

The assistant needs a tool definition to create Salesforce leads. This is where 90% of implementations break - they don't validate required fields before calling Salesforce.

Tool Definition (add to assistantConfig):

javascript
assistantConfig.model.tools = [{
  type: "function",
  function: {
    name: "create_lead",
    description: "Create a lead in Salesforce CRM. Only call when you have ALL required fields.",
    parameters: {
      type: "object",
      properties: {
        firstName: { type: "string" },
        lastName: { type: "string" },
        phone: { type: "string" },
        propertyType: { type: "string", enum: ["house", "condo", "land"] },
        budget: { type: "number" },
        location: { type: "string" },
        timeline: { type: "string", enum: ["immediate", "1-3months", "3-6months"] }
      },
      required: ["firstName", "lastName", "phone", "propertyType", "budget", "location", "timeline"]
    }
  }
}];

Webhook Handler

javascript
app.post('/webhook/vapi', async (req, res) => {
  const { message } = req.body;
  
  // Handle function call from assistant
  if (message?.type === 'function-call' && message?.functionCall?.name === 'create_lead') {
    const params = message.functionCall.parameters;
    
    try {
      // Create Salesforce lead - REAL API call
      const sfResponse = await fetch(`${salesforceConfig.instanceUrl}/services/data/v58.0/sobjects/Lead`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          FirstName: params.firstName,
          LastName: params.lastName,
          Phone: params.phone,
          Company: 'Real Estate Lead', // Required field
          Property_Type__c: params.propertyType, // Custom field
          Budget__c: params.budget,
          Location__c: params.location,
          Timeline__c: params.timeline,
          LeadSource: 'Voice AI'
        })
      });
      
      if (!sfResponse.ok) {
        const error = await sfResponse.json();
        throw new Error(`Salesforce error: ${error[0]?.message}`);
      }
      
      const leadData = await sfResponse.json();
      
      // Return success to Vapi
      return res.json({
        result: `Lead created successfully with ID ${leadData.id}. I've logged your information and an agent will contact you within 24 hours.`
      });
      
    } catch (error) {
      console.error('Lead creation failed:', error);
      return res.json({
        result: "I encountered an issue saving your information. Let me transfer you to a live agent."
      });
    }
  }
  
  res.sendStatus(200);
});

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

Critical: Salesforce's REST API returns 201 Created on success, but 400 if required fields are missing. The assistant MUST collect all 7 fields before calling the function, or you'll get partial leads that break your CRM workflow.

System Diagram

State machine showing vapi call states and transitions.

mermaid
stateDiagram-v2
    [*] --> Idle
    Idle --> Listening: User speaks
    Listening --> Processing: EndOfTurn detected
    Processing --> Responding: LLM response ready
    Responding --> Listening: TTS complete
    Responding --> Idle: Barge-in detected
    Listening --> Idle: Timeout
    Processing --> Error: API failure
    Error --> Idle: Retry
    Error --> Idle: Max retries reached
    Processing --> Error: Model configuration error
    Listening --> Error: Speech recognition error
    Error --> Listening: Recoverable error
    Idle --> Error: Initialization failure
    Error --> Idle: Reset system

Testing & Validation

Local Testing

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

Expose your local server:

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

Update your assistant config with the ngrok URL:

javascript
const assistantConfig = {
  model: {
    provider: "openai",
    model: "gpt-4",
    messages: [{ role: "system", content: "Qualify real estate leads" }]
  },
  voice: { provider: "11labs", voiceId: "21m00Tcm4TlvDq8ikWAM" },
  transcriber: { provider: "deepgram", language: "en" },
  serverUrl: "https://abc123.ngrok.io/webhook/vapi", // YOUR server endpoint
  serverUrlSecret: process.env.VAPI_SECRET
};

Test the full flow with curl:

bash
curl -X POST https://api.vapi.ai/call \
  -H "Authorization: Bearer $VAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "assistantId": "your-assistant-id",
    "customer": { "number": "+15551234567" }
  }'

Watch your server logs. If you see POST /webhook/vapi 200 but no Salesforce lead, your function calling params are malformed.

Webhook Validation

Verify webhook signatures to prevent replay attacks:

javascript
app.post('/webhook/vapi', express.json(), (req, res) => {
  const signature = req.headers['x-vapi-signature'];
  const payload = JSON.stringify(req.body);
  
  const crypto = require('crypto');
  const expectedSig = crypto
    .createHmac('sha256', process.env.VAPI_SECRET)
    .update(payload)
    .digest('hex');
  
  if (signature !== expectedSig) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Process webhook safely
  res.status(200).json({ received: true });
});

Common failure: Missing Content-Type: application/json in webhook config. VAPI sends JSON, but if your server expects form data, parsing fails silently. Check req.body is an object, not undefined.

Real-World Example

Barge-In Scenario

Most real estate voice agents break when prospects interrupt mid-pitch. Here's what happens when a lead cuts off your agent describing a 3-bedroom listing:

javascript
// Vapi webhook handler - Barge-in detection
app.post('/webhook/vapi', async (req, res) => {
  const { message } = req.body;
  
  if (message.type === 'transcript' && message.transcriptType === 'partial') {
    // Prospect interrupts: "Wait, I need 4 bedrooms"
    const interruptDetected = message.transcript.length > 10 && 
                             message.role === 'user';
    
    if (interruptDetected) {
      // Cancel current TTS immediately - don't let agent finish listing details
      await fetch(`https://api.vapi.ai/call/${message.call.id}/control`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.VAPI_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          action: 'interrupt',
          reason: 'user_barge_in'
        })
      });
      
      // Update Salesforce Lead with interrupt context
      const leadData = {
        Id: message.call.metadata.salesforceLeadId,
        Interrupt_Count__c: (message.call.metadata.interruptCount || 0) + 1,
        Last_Interrupt_Text__c: message.transcript,
        Engagement_Score__c: 'High' // Interruptions = engaged prospect
      };
      
      await updateSalesforceRecord('Lead', leadData, accessToken);
    }
  }
  
  res.sendStatus(200);
});

Event Logs

Timestamp: 14:23:41.203 - Agent starts: "This property features three spacious bedrooms with—"
Timestamp: 14:23:42.891 - Partial transcript: { role: 'user', transcript: 'Wait I need', transcriptType: 'partial' }
Timestamp: 14:23:42.903 - Interrupt triggered, TTS cancelled
Timestamp: 14:23:43.127 - Salesforce updated: Interrupt_Count__c: 1, Engagement_Score__c: 'High'
Timestamp: 14:23:43.450 - Agent pivots: "Got it, looking for four bedrooms. What's your target neighborhood?"

Edge Cases

Multiple rapid interrupts ("Wait—no, actually—hold on"): Track Interrupt_Count__c in Salesforce. If count > 3 in 30 seconds, flag lead as Needs_Human_Followup__c = true. Don't let the bot spiral.

False positives (background noise, "uh-huh"): Set transcriber.endpointing.minSpeechDuration to 800ms minimum. Filters out acknowledgment sounds that aren't real interruptions. Without this, you'll cancel TTS on every breath.

Partial transcript lag: Mobile networks add 150-300ms jitter. Buffer partial transcripts for 200ms before triggering interrupt logic. Otherwise, you'll cut off the agent when the user was just pausing mid-thought.

Common Issues & Fixes

Most Salesforce-VAPI integrations break when webhooks fire faster than your CRM can process them. The function-call event triggers while the previous lead update is still pending, causing duplicate records or lost data.

Race Condition: VAPI sends function-call → your server calls Salesforce → before the response returns, user interrupts → second function-call fires → Salesforce receives two identical leads with different timestamps.

javascript
// Production-grade queue to prevent race conditions
const processingQueue = new Map();

app.post('/webhook/vapi', async (req, res) => {
  const { message } = req.body;
  const callId = message.call?.id;
  
  if (processingQueue.has(callId)) {
    console.warn(`Call ${callId} already processing, queuing...`);
    return res.status(202).json({ queued: true });
  }
  
  processingQueue.set(callId, true);
  
  try {
    if (message.type === 'function-call') {
      const { firstName, lastName, phone, propertyType } = message.functionCall.parameters;
      
      const sfResponse = await fetch(`${salesforceConfig.instanceUrl}/services/data/v58.0/sobjects/Lead`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          FirstName: firstName,
          LastName: lastName,
          Phone: phone,
          Company: propertyType,
          LeadSource: 'Voice AI'
        })
      });
      
      if (!sfResponse.ok) {
        throw new Error(`Salesforce API error: ${sfResponse.status}`);
      }
      
      res.json({ result: 'Lead created successfully' });
    }
  } catch (error) {
    console.error('Webhook processing failed:', error);
    res.status(500).json({ error: error.message });
  } finally {
    processingQueue.delete(callId);
  }
});

Why This Works: The processingQueue Map tracks active calls. If a second webhook arrives before the first completes, we return HTTP 202 (Accepted) immediately instead of processing duplicate data.

Token Expiration: OAuth tokens expire after 2 hours. If your call runs longer, lead creation fails with HTTP 401.

javascript
async function ensureValidToken() {
  const tokenAge = Date.now() - tokenTimestamp;
  
  if (tokenAge > 5400000) {
    const response = await fetch(salesforceConfig.tokenEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: process.env.SALESFORCE_REFRESH_TOKEN,
        client_id: process.env.SALESFORCE_CLIENT_ID,
        client_secret: process.env.SALESFORCE_CLIENT_SECRET
      })
    });
    
    const data = await response.json();
    accessToken = data.access_token;
    tokenTimestamp = Date.now();
  }
  
  return accessToken;
}

Call await ensureValidToken() before every Salesforce API request to prevent mid-call authentication failures.

Incomplete Data from Interruptions: Users interrupt mid-sentence: "I'm looking for a three-bed—actually, make that four bedrooms." Your function gets called with propertyType: "three-bed" before the correction.

javascript
let interruptDetected = false;

app.post('/webhook/vapi', async (req, res) => {
  const { message } = req.body;
  
  if (message.type === 'speech-update' && message.status === 'interrupted') {
    interruptDetected = true;
    console.log('User interrupted, delaying lead creation');
    return res.json({ action: 'wait' });
  }
  
  if (message.type === 'function-call' && !interruptDetected) {
    const params = message.functionCall.parameters;
    const sfResponse = await fetch(`${salesforceConfig.instanceUrl}/services/data/v58.0/sobjects/Lead`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        FirstName: params.firstName,
        LastName: params.lastName,
        Phone: params.phone,
        Company: params.propertyType,
        LeadSource: 'Voice AI'
      })
    });
  }
  
  if (interruptDetected) {
    setTimeout(() => { interruptDetected = false; }, 3000);
  }
  
  res.json({ result: 'processed' });
});

Production Note: Store interruptDetected in Redis with callId as key. In-memory state breaks with multiple server instances behind a load balancer.

Complete Working Example

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

Full Server Code

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

app.use(express.json());

// Salesforce OAuth configuration
const salesforceConfig = {
  tokenEndpoint: 'https://login.salesforce.com/services/oauth2/token',
  clientId: process.env.SF_CLIENT_ID,
  clientSecret: process.env.SF_CLIENT_SECRET,
  redirectUri: process.env.SF_REDIRECT_URI
};

let accessToken = null;
let tokenAge = Date.now();

// Token refresh logic - Salesforce tokens expire after 2 hours
async function ensureValidToken() {
  if (accessToken && (Date.now() - tokenAge < 7200000)) {
    return accessToken;
  }
  
  const response = await fetch(salesforceConfig.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: salesforceConfig.clientId,
      client_secret: salesforceConfig.clientSecret,
      refresh_token: process.env.SF_REFRESH_TOKEN
    })
  });
  
  if (!response.ok) throw new Error(`Token refresh failed: ${response.status}`);
  const data = await response.json();
  accessToken = data.access_token;
  tokenAge = Date.now();
  return accessToken;
}

// OAuth initiation - redirects user to Salesforce login
app.get('/oauth/login', (req, res) => {
  const authUrl = `https://login.salesforce.com/services/oauth2/authorize?` +
    `response_type=code&client_id=${salesforceConfig.clientId}&` +
    `redirect_uri=${encodeURIComponent(salesforceConfig.redirectUri)}`;
  res.redirect(authUrl);
});

// OAuth callback - exchanges code for tokens
app.get('/oauth/callback', async (req, res) => {
  try {
    const response = await fetch(salesforceConfig.tokenEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code: req.query.code,
        client_id: salesforceConfig.clientId,
        client_secret: salesforceConfig.clientSecret,
        redirect_uri: salesforceConfig.redirectUri
      })
    });
    
    const data = await response.json();
    accessToken = data.access_token;
    tokenAge = Date.now();
    
    // Store refresh_token in environment for production
    console.log('Refresh token (save to .env):', data.refresh_token);
    res.send('OAuth complete. Check console for refresh token.');
  } catch (error) {
    res.status(500).send(`OAuth failed: ${error.message}`);
  }
});

// Webhook handler - processes function call results from Vapi
app.post('/webhook', async (req, res) => {
  const payload = JSON.stringify(req.body);
  const signature = req.headers['x-vapi-signature'];
  
  // Verify webhook signature to prevent spoofing
  const expectedSig = crypto
    .createHmac('sha256', process.env.VAPI_SERVER_SECRET)
    .update(payload)
    .digest('hex');
  
  if (signature !== expectedSig) {
    return res.status(401).send('Invalid signature');
  }
  
  const { type, params } = req.body;
  
  if (type === 'function-call' && params.name === 'createLead') {
    try {
      const token = await ensureValidToken();
      
      const leadData = {
        FirstName: params.firstName,
        LastName: params.lastName,
        Phone: params.phone,
        Company: params.propertyType || 'Unknown',
        LeadSource: 'Voice AI',
        Budget__c: params.budget,
        Location__c: params.location,
        Timeline__c: params.timeline,
        Engagement_Score__c: 85 // High score for voice-qualified leads
      };
      
      const sfResponse = await fetch(
        `${process.env.SF_INSTANCE_URL}/services/data/v58.0/sobjects/Lead`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(leadData)
        }
      );
      
      if (!sfResponse.ok) {
        const error = await sfResponse.json();
        return res.json({ 
          result: { failed: true, error: error[0].message }
        });
      }
      
      const result = await sfResponse.json();
      res.json({ result: { leadId: result.id } });
      
    } catch (error) {
      res.json({ result: { failed: true, error: error.message } });
    }
  } else {
    res.json({ result: {} });
  }
});

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

Run Instructions

Prerequisites: Node.js 18+, ngrok for webhook tunneling, Salesforce Developer account

Setup steps:

  1. Install dependencies: npm install express node-fetch
  2. Create .env file with: SF_CLIENT_ID, SF_CLIENT_SECRET, SF_REDIRECT_URI=http://localhost:3000/oauth/callback, SF_INSTANCE_URL, VAPI_SERVER_SECRET
  3. Start server: node server.js
  4. Run ngrok: ngrok http 3000 (copy HTTPS URL)
  5. Complete OAuth: Visit http://localhost:3000/oauth/login, save refresh token from console to .env as SF_REFRESH_TOKEN
  6. Update Vapi assistant's serverUrl to your ngrok URL + /webhook

Critical production changes: Replace ngrok with a permanent domain, store refresh tokens in encrypted database (not .env), implement rate limiting on /webhook (100 req/min max), add request logging for debugging failed Salesforce writes, set up token refresh monitoring (alert if refresh fails 3x in a row).

Common failure: If leads aren't creating, check Salesforce field-level security - the API user needs write access to custom fields like Budget__c. Verify with: GET /services/data/v58.0/sobjects/Lead/describe and check fields[].createable.

FAQ

Technical Questions

How do I authenticate VAPI calls to Salesforce without exposing API keys?

Use OAuth 2.0 with a secure token refresh mechanism. Store accessToken in an encrypted session, not in client-side code. Implement ensureValidToken() to check tokenAge before each Salesforce API call—if the token is older than 55 minutes (Salesforce tokens expire at 60), refresh it using tokenEndpoint with your grant_type: "refresh_token". This prevents hardcoding credentials and keeps your integration compliant with Salesforce security policies.

What's the difference between webhook-based and polling-based lead updates?

Webhooks are event-driven: VAPI sends call transcripts and intent data to your server endpoint immediately after the call ends. Polling requires your server to repeatedly query Salesforce for changes, wasting API calls and introducing latency. For real estate, webhooks are mandatory—you need lead qualification data in Salesforce within seconds, not minutes. Configure your serverUrl in VAPI to receive POST requests with payload containing transcription and extracted fields like propertyType, budget, and timeline.

How do I validate webhook signatures from VAPI?

VAPI includes a signature header in webhook requests. Use crypto.createHmac() to compute expectedSig from the raw request body and your webhook secret. Compare it to the incoming signature—if they don't match, reject the request. This prevents malicious actors from spoofing lead data into your Salesforce instance.

Performance

Why is my lead qualification taking 8+ seconds per call?

Three bottlenecks: (1) STT latency (2-3s), (2) LLM inference (1-2s), (3) Salesforce API round-trip (2-3s). Reduce this by using partial transcripts—process intent recognition on onPartialTranscript events instead of waiting for the full transcript. This lets you start Salesforce lookups while the caller is still speaking, cutting total latency to 3-4 seconds.

How do I handle barge-in without cutting off the agent mid-sentence?

Set transcriber.endpointing to detect caller speech, which triggers interruptDetected. When true, stop TTS playback immediately and flush the audio buffer. Don't just lower volume—actually cancel the synthesis request. This prevents the agent from talking over the caller, which kills conversion rates in real estate calls.

Platform Comparison

Should I use VAPI or Twilio for voice agent integration?

VAPI is purpose-built for AI voice agents with native LLM integration and function calling. Twilio is a carrier-grade telephony platform. Use VAPI for the agent logic (transcription, intent recognition, response generation) and Twilio for PSTN connectivity if you need inbound/outbound calling to real phone numbers. VAPI handles the intelligence; Twilio handles the pipes. For Salesforce integration, VAPI's webhook system is cleaner than Twilio's event model.

Can I use intent recognition from VAPI instead of building custom extraction logic?

Yes. Configure your assistantConfig with a system prompt that instructs the LLM to extract structured data (firstName, lastName, propertyType, budget, timeline) and return it as JSON. VAPI's function calling lets you define a parameters schema with required fields. This is faster than post-processing transcripts with regex or NLP libraries, and it keeps logic in one place.

Resources

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

Official Documentation

Implementation Repos & Tools

  • VAPI GitHub Examples – Production webhook handlers, function calling patterns
  • Salesforce CLI – Local org testing, metadata deployment
  • ngrok – Secure webhook tunneling for local development

Key Integration Patterns

  • Voice agent webhooks for real-time lead capture
  • OAuth 2.0 token refresh cycles (Salesforce tokenEndpoint)
  • Intent recognition via function calling (assistantConfig.functions)
  • CRM transcription storage in Salesforce custom fields (Engagement_Score__c)

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
  7. 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.