고객 지원 강화를 위한 Voice AI와 Salesforce 통합
Source: Dev.to
TL;DR
대부분의 Salesforce 음성 통합은 호출량이 급증하거나 CRM 쿼리가 타임아웃될 때 중단됩니다. 이 가이드는 VAPI의 음성 AI를 Twilio를 통해 Salesforce와 연결하여 데이터 손실 없이 1000개 이상의 동시 호출을 처리할 수 있는 견고한 통합을 구축하는 방법을 보여줍니다. 시스템은 실시간 케이스 조회, 통화 중 연락처 레코드 업데이트, 전사 자동 로그를 수행하여 해결 시간을 단축하고 수동 데이터 입력을 없앱니다.
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 폴리필 불필요). - Public HTTPS endpoint – Ngrok 또는 프로덕션 도메인으로 webhook 콜백을 받음.
- Salesforce profile permissions – API Enabled, View All Data, Modify All Data (Case/Contact CRUD 권한).
System Constraints
- Webhook timeout tolerance – Salesforce OAuth 토큰 갱신에 200‑400 ms 지연이 추가됩니다.
- Rate limits – Salesforce: 사용자당 20 초에 100 API 호출; VAPI: 계정당 동시 50 통화.
- Sandbox note – Sandbox 조직은 다른 OAuth 엔드포인트(
test.salesforce.com)를 사용합니다.login.salesforce.com을 하드코딩하면 Sandbox 환경에서 작동하지 않습니다.
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
};
왜 중요한가: functions 배열은 GPT‑4가 구조화된 파라미터와 함께 Salesforce 쓰기를 언제 트리거할지 알려 주어, 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으로 로컬 테스트를 수행하세요.