고객 지원 강화를 위한 Voice AI와 Salesforce 통합

발행: (2025년 12월 3일 오전 09:01 GMT+9)
7 min read
원문: Dev.to

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으로 로컬 테스트를 수행하세요.

Back to Blog

관련 글

더 보기 »

계정 전환

@blink_c5eb0afe3975https://dev.to/blink_c5eb0afe3975 여러분도 알다시피 저는 다시 제 진행 상황을 기록하기 시작했으니, 이것을 다른…