将 Voice AI 与 Salesforce 集成,实现增强的客户支持
Source: Dev.to
TL;DR
大多数 Salesforce 语音集成在通话量激增或 CRM 查询超时时会出现故障。本文指南展示了如何构建一个稳健的集成,能够在不丢失数据的情况下处理 1000+ 并发通话,将 VAPI 的语音 AI 通过 Twilio 连接到 Salesforce。系统实现实时案件查询、通话中更新联系人记录,并自动记录转录,从而缩短解决时间并实现零手动数据录入。
Stack: VAPI(语音 AI)、Twilio(电话)、Salesforce REST API(CRM 操作)、Node.js webhook 服务器(编排层)。
API Access & Credentials
- VAPI API Key – 已启用电话号码供应的生产账户。
- Twilio Account SID & Auth Token – 确认账户未处于试用模式(试用模式会阻止外呼)。
- Salesforce Connected App – OAuth 2.0 凭证(Client ID、Client Secret、Refresh Token)。
- Salesforce API Version – v58.0 或更高(实时事件流所必需)。
Technical Requirements
- Node.js 18+ – 需要原生
fetch支持(不使用 Axios polyfills)。 - Public HTTPS endpoint – 使用 Ngrok 或生产域名提供 webhook 回调。
- Salesforce profile permissions – API Enabled、View All Data、Modify All Data(用于 Case/Contact 的增删改查)。
System Constraints
- Webhook timeout tolerance – Salesforce OAuth 令牌刷新会增加 200‑400 ms 延迟。
- Rate limits – Salesforce:每位用户每 20 秒最多 100 次 API 调用;VAPI:每个账户最多 50 个并发通话。
- Sandbox note – 沙盒组织使用不同的 OAuth 端点(
test.salesforce.com)。硬编码login.salesforce.com会在沙盒环境中失效。
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 负责语音转录和合成。
- Your server 充当 VAPI 与 Salesforce 之间的桥梁,包含所有 CRM 逻辑。
- Do NOT 让 VAPI 直接调用 Salesforce;那样会绕过身份验证和错误处理。
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 令牌在 2 小时后过期。请实现刷新逻辑或维护令牌池;单次请求模式在令牌失效后会出错。
- Rate‑limit handling: 对
429响应加入指数退避并重试。
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);
验证 webhook 能返回包含新建案例 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]
常见故障点:静默的 webhook 错误、缺失签名或令牌过期。务必在将系统投入生产前先使用 Ngrok 在本地进行完整测试。