Integrate Voice AI with Salesforce for Enhanced Customer Support

Published: (December 2, 2025 at 07:01 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

TL;DR

Most Salesforce voice integrations break when call volume spikes or CRM queries timeout. This guide shows how to build a robust integration that handles 1000+ concurrent calls without data loss, connecting VAPI’s voice AI to Salesforce via Twilio. The system performs real‑time case lookups, updates contact records mid‑call, and logs transcripts automatically, resulting in faster resolution times and zero manual data entry.

Stack: VAPI (voice AI), Twilio (telephony), Salesforce REST API (CRM operations), Node.js webhook server (orchestration layer).

API Access & Credentials

  • VAPI API Key – Production account with phone number provisioning enabled.
  • Twilio Account SID & Auth Token – Verify the account is not in trial mode (trial blocks outbound calls).
  • Salesforce Connected App – OAuth 2.0 credentials (Client ID, Client Secret, Refresh Token).
  • Salesforce API Version – v58.0 or newer (required for real‑time event streaming).

Technical Requirements

  • Node.js 18+ – Native fetch support required (no Axios polyfills).
  • Public HTTPS endpoint – Ngrok or a production domain for webhook callbacks.
  • Salesforce profile permissions – API Enabled, View All Data, Modify All Data (for Case/Contact CRUD).

System Constraints

  • Webhook timeout tolerance – Salesforce OAuth token refresh adds 200‑400 ms latency.
  • Rate limits – Salesforce: 100 API calls per 20 seconds per user; VAPI: 50 concurrent calls per account.
  • Sandbox note – Sandbox orgs use different OAuth endpoints (test.salesforce.com). Hard‑coding login.salesforce.com will break in sandbox environments.

Data Flow Overview

flowchart LR
    A[Customer Call] --> B[Twilio]
    B --> C[VAPI Voice Agent]
    C --> D[Your Webhook Server]
    D --> E[Salesforce API]
    E --> D
    D --> C
    C --> B
    B --> A
  • VAPI handles voice transcription and synthesis.
  • Your server bridges VAPI to Salesforce, containing all CRM logic.
  • Do NOT let VAPI call Salesforce directly; this bypasses authentication and error handling.

VAPI Assistant Configuration

const assistantConfig = {
  model: {
    provider: "openai",
    model: "gpt-4",
    messages: [{
      role: "system",
      content: "You are a customer support agent. Extract: customer name, issue type, account number. Confirm details before creating case."
    }],
    functions: [{
      name: "createSalesforceCase",
      description: "Creates support case in Salesforce CRM",
      parameters: {
        type: "object",
        properties: {
          accountNumber: { type: "string" },
          issueType: {
            type: "string",
            enum: ["billing", "technical", "account"]
          },
          description: { type: "string" }
        },
        required: ["accountNumber", "issueType", "description"]
      }
    }]
  },
  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.WEBHOOK_SECRET
};

Why it matters: The functions array tells GPT‑4 when to trigger Salesforce writes with well‑structured parameters, preventing garbage data from entering the CRM.

Webhook Server (Node.js)

// server.js
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Validate webhook signature – prevents unauthorized CRM writes
function validateSignature(req) {
  const signature = req.headers['x-vapi-signature'];
  const payload = JSON.stringify(req.body);
  const hash = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  return signature === hash;
}

app.post('/webhook/vapi', async (req, res) => {
  if (!validateSignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { message } = req.body;

  // Handle VAPI function‑call requests
  if (
    message.type === 'function-call' &&
    message.functionCall.name === 'createSalesforceCase'
  ) {
    const { accountNumber, issueType, description } = message.functionCall.parameters;

    try {
      // Obtain Salesforce OAuth token
      const authResponse = 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.SF_CLIENT_ID,
          client_secret: process.env.SF_CLIENT_SECRET
        })
      });

      if (!authResponse.ok) throw new Error(`Salesforce auth failed: ${authResponse.status}`);
      const { access_token, instance_url } = await authResponse.json();

      // Create the case in Salesforce
      const caseResponse = await fetch(`${instance_url}/services/data/v58.0/sobjects/Case`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${access_token}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          AccountNumber: accountNumber,
          Type: issueType,
          Description: description,
          Origin: 'Phone',
          Status: 'New'
        })
      });

      if (!caseResponse.ok) throw new Error(`Case creation failed: ${caseResponse.status}`);
      const caseData = await caseResponse.json();

      // Respond to VAPI – the agent will speak this to the caller
      return res.json({
        result: `Case ${caseData.id} created. Reference number: ${caseData.CaseNumber}`
      });
    } catch (error) {
      console.error('Salesforce API Error:', error);
      return res.json({
        result: "System error. Case not created. Please call back."
      });
    }
  }

  // Fallback for other messages
  res.json({ received: true });
});

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

Production Considerations

  • Token expiry: Salesforce OAuth tokens expire after 2 hours. Implement refresh logic or maintain a token pool; the simple request‑per‑call pattern will fail after expiry.
  • Rate‑limit handling: Add exponential back‑off and retry for 429 responses.

Local Testing with Ngrok

ngrok http 3000
# Use the generated HTTPS URL as `serverUrl` in `assistantConfig`

Simulating a VAPI Payload

// test-webhook.js – simulate VAPI function call
const crypto = require('crypto');
const fetch = require('node-fetch');

const payload = JSON.stringify({
  message: {
    type: 'function-call',
    functionCall: {
      name: 'createSalesforceCase',
      parameters: {
        accountNumber: 'ACC-12345',
        issueType: 'billing',
        description: 'Customer reports unexpected charge on invoice.'
      }
    }
  }
});

const signature = crypto
  .createHmac('sha256', process.env.WEBHOOK_SECRET)
  .update(payload)
  .digest('hex');

fetch('https://.ngrok.io/webhook/vapi', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-vapi-signature': signature
  },
  body: payload
})
  .then(res => res.json())
  .then(console.log)
  .catch(console.error);

Verify that the webhook returns a success message with the newly created case ID.

Audio Processing Pipeline

graph LR
    A[Microphone] --> B[Audio Buffer]
    B --> C[Voice Activity Detection]
    C -->|Speech Detected| D[Speech‑to‑Text]
    C -->|No Speech| E[Error: No Input Detected]
    D --> F[Large Language Model]
    F --> G[Response Generation]
    G --> H[Text‑to‑Speech]
    H --> I[Speaker]
    D -->|Error: Unrecognized Speech| J[Error Handling]
    F -->|Error: Processing Failed| J
    J --> K[Log Error]
    K --> L[Notify User]

Common failure points: silent webhook errors, missing signatures, or token expiration. Always test locally with Ngrok before moving to production.

Back to Blog

Related posts

Read more »