CRM에 통화 기록 남기기: 전화번호로 고객 매칭하고 요약 붙이기
KAT Cloud의 CALL_COMPLETED 웹훅으로 CRM에 통화 기록을 남기는 방법입니다. isInbound에 따라 fromNumber와 toNumber 중 고객 번호를 고르고, 번호를 정규화해 고객을 찾은 뒤 요약과 대화 내역을 활동 기록으로 붙입니다.

CRM에 통화 기록을 남기는 핵심은 KAT Cloud CALL_COMPLETED 웹훅의 isInbound 값으로 고객 번호를 고르는 것입니다. 수신 통화(isInbound: true)는 fromNumber가, 발신 통화(isInbound: false)는 toNumber가 고객 번호입니다. 이 번호를 CRM과 같은 형식으로 정규화해 고객을 찾고, summary와 sttAI·sttDirect 대화 내역을 활동 기록으로 붙이면 어떤 CRM을 쓰든 같은 구조로 연동할 수 있습니다.
연동 구조: 웹훅, 매칭, 활동 기록
KAT Cloud(캣클라우드)는 통화가 끝나고 STT·요약 분석이 완료되면 CALL_COMPLETED 이벤트를 등록된 URL로 보냅니다. 연동 서버는 이 이벤트를 받아 세 가지 일을 합니다. 고객 번호를 고르고 정규화하기, CRM에서 고객 찾기, 찾은 고객에게 활동 기록 남기기입니다. 특정 CRM의 API는 마지막 단계의 어댑터 안에만 두고, 나머지는 공통 코드로 만듭니다.

CRM 연동에 쓰는 CALL_COMPLETED 필드는 다음과 같습니다.
| 용도 | 필드 | 메모 |
|---|---|---|
| 중복 방지 키 | callHistoryId | CRM 활동의 외부 ID로 저장 |
| 고객 번호 선택 | isInbound, fromNumber, toNumber | 방향에 따라 고객 번호가 다름 |
| 통화 결과 | status, tag | 응답·부재중, AI·직원 응대 구분 |
| 통화 시간 | duration | 초 단위 |
| 기록 본문 | summary, sttAI, sttDirect | 요약과 구간별 대화 내역 |
| 녹음 | recordingUrl | 선택 필드. 만료를 고려해 보관 |
| 시각 | startedAt, completedAt | ISO 8601 |
웹훅 등록은 POST /webhook에 eventType: CALL_COMPLETED와 https URL을 보내면 되며, 웹훅 관리 API는 비즈니스 요금제 그룹 또는 별도 허용된 그룹에서 쓸 수 있습니다. 필드 정의는 웹훅 이벤트 문서에 있습니다.
export KAT_API_KEY="콘솔에서 발급받은 키"
curl -s -X POST "https://studio-gateway.zerocall.kr/webhook" \
-H "x-api-key: $KAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"eventType":"CALL_COMPLETED","url":"https://crm-sync.example.com/webhook/call-completed"}'1단계: 고객 번호 고르고 정규화하기
KAT Cloud 웹훅의 fromNumber, toNumber는 01012345678처럼 숫자만 들어 있습니다. 반면 CRM에는 010-1234-5678, +82 10-1234-5678, (02) 123-4567처럼 사람이 입력한 형식이 섞여 있는 경우가 많습니다. 양쪽을 같은 규칙으로 정규화해야 매칭이 됩니다.
| isInbound | 고객 번호 | 내 번호(KAT Cloud 번호) |
|---|---|---|
true(수신 통화) | fromNumber | toNumber |
false(발신 통화) | toNumber | fromNumber |
발신 통화는 SIP로 연결한 내 음성 서버나 PBX가 KAT Cloud 회선으로 건 통화입니다. 방향을 잘못 읽으면 모든 발신 통화가 내 회사 번호로 매칭되므로, 이 표를 코드로 그대로 옮깁니다.
// phone.js — 국내 전화번호 정규화
function normalizeKrPhone(raw) {
if (!raw) return null;
let d = String(raw).replace(/\D/g, ''); // 숫자만 남기기
if (d.startsWith('82') && d.length >= 10) {
// +82 10-1234-5678 → 01012345678, +82 2-123-4567 → 021234567
d = d.slice(2);
if (!d.startsWith('0')) d = '0' + d;
}
return d.length >= 8 ? d : null; // 1588-0000 같은 8자리 대표번호까지 허용
}
function pickNumbers(call) {
const customer = call.isInbound ? call.fromNumber : call.toNumber;
const ours = call.isInbound ? call.toNumber : call.fromNumber;
return { customer: normalizeKrPhone(customer), ours: normalizeKrPhone(ours) };
}
module.exports = { normalizeKrPhone, pickNumbers };CRM 쪽도 저장할 때 같은 함수로 정규화한 번호를 별도 필드(예: phone_normalized)에 넣어 두면 검색이 정확하고 빨라집니다. 기존 데이터는 한 번 일괄 변환합니다.
2단계: CRM 어댑터로 고객 찾기
CRM마다 API는 다르지만 통화 기록 연동에 필요한 동작은 세 가지로 같습니다. 이 세 함수만 사용하는 CRM의 API로 구현하면 나머지 코드는 바꿀 필요가 없습니다.
// crm-adapter.js — 사용하는 CRM의 API로 채워 넣는 부분
module.exports = {
// 정규화된 번호로 고객 목록을 찾는다: [{ id, name, lastActivityAt }]
async findContactsByPhone(phone) {
return [];
},
// 매칭되는 고객이 없을 때 리드(잠재 고객)를 만든다: { id }
async createLead({ phone, source }) {
return { id: 'lead-id' };
},
// externalId가 같은 활동이 있으면 갱신, 없으면 생성한다
async upsertActivity({ externalId, contactId, subject, body, fields, occurredAt }) {},
};검색 결과는 세 경우로 나뉩니다. 한 번호를 가족이나 회사 동료가 같이 쓰는 경우도 있으므로 여러 명이 나오는 경우를 따로 처리합니다.
| 검색 결과 | 처리 방법 |
|---|---|
| 1명 | 그 고객에게 활동 기록을 남깁니다 |
| 여러 명 | 최근 활동이 있는 고객에게 남기고, 후보 목록과 "담당자 확인" 표시를 함께 기록합니다 |
| 없음 | 리드를 새로 만들거나, 미매칭 대기열에 넣어 담당자가 확인하게 합니다 |
// match.js
async function matchContact(crm, phone) {
if (!phone) return { contactId: null, matchType: 'NO_NUMBER' };
const found = await crm.findContactsByPhone(phone);
if (found.length === 1) return { contactId: found[0].id, matchType: 'EXACT' };
if (found.length > 1) {
const latest = [...found].sort((a, b) =>
String(b.lastActivityAt || '').localeCompare(String(a.lastActivityAt || '')))[0];
return { contactId: latest.id, matchType: 'MULTIPLE', candidates: found.map((c) => c.id) };
}
const lead = await crm.createLead({ phone, source: 'KAT Cloud 통화' });
return { contactId: lead.id, matchType: 'NEW_LEAD' };
}
module.exports = { matchContact };모르는 번호마다 리드를 만들면 스팸 전화나 잘못 건 전화까지 리드가 됩니다. 응답한 통화(status: ANSWERED)이면서 통화 시간이 일정 이상일 때만 리드를 만드는 식으로 조건을 거는 것도 방법입니다.
3단계: 요약과 대화 내역을 활동 기록으로 붙이기
활동 기록의 제목에는 방향, 응대 태그, 통화 시간을 넣고, 본문에는 요약과 대화 내역을 넣습니다. 대화 내역은 sttAI(AI 응대 구간)와 sttDirect(직원 응대 구간)를 구간 표시와 함께 이어 붙입니다. 각 항목의 role은 agent(AI 또는 직원)와 customer(고객)이고, timestamp는 초 단위입니다. 데이터 구조는 통화 받아쓰기와 요약 데이터 구조 글에서 자세히 다룹니다.
// activity.js
const TAG = { AI: 'AI 응대', DIRECT: '직원 응대', AI_TO_DIRECT: 'AI→직원 연결', UNKNOWN: '확인 불가' };
function transcriptText(call) {
const lines = [];
for (const t of call.sttAI || []) {
lines.push(`[AI 구간 ${t.timestamp}s] ${t.role === 'agent' ? 'AI' : '고객'}: ${t.text}`);
}
for (const t of call.sttDirect || []) {
lines.push(`[직원 구간 ${t.timestamp}s] ${t.role === 'agent' ? '직원' : '고객'}: ${t.text}`);
}
return lines.join('\n');
}
function buildActivity(call, match) {
const direction = call.isInbound ? '수신' : '발신';
return {
externalId: `katcloud:${call.callHistoryId}`, // 같은 통화는 같은 활동으로 upsert
contactId: match.contactId,
subject: `[${direction} 통화] ${TAG[call.tag] || '확인 불가'} · ${call.duration || 0}초`,
body: [
`요약: ${call.summary || '(요약 없음)'}`,
'',
'대화 내역',
transcriptText(call) || '(대화 내역 없음)',
].join('\n'),
fields: {
direction,
status: call.status,
tag: call.tag,
durationSec: call.duration,
matchType: match.matchType,
candidates: match.candidates || [],
},
occurredAt: call.startedAt,
};
}
module.exports = { buildActivity };웹훅 서버는 앞의 조각을 묶기만 합니다. 즉시 200을 돌려주고, 매칭과 기록은 응답 뒤에 처리합니다.
// server.js — Node.js 18+, Express 4
const express = require('express');
const crm = require('./crm-adapter');
const { pickNumbers } = require('./phone');
const { matchContact } = require('./match');
const { buildActivity } = require('./activity');
const app = express();
app.use(express.json({ limit: '2mb' })); // STT 배열이 길 수 있다
app.post('/webhook/call-completed', (req, res) => {
res.sendStatus(200); // KAT Cloud 웹훅 타임아웃은 10초
const { eventType, data } = req.body || {};
if (eventType !== 'CALL_COMPLETED' || !data?.callHistoryId) return;
setImmediate(async () => {
try {
const { customer } = pickNumbers(data);
const match = await matchContact(crm, customer);
await crm.upsertActivity(buildActivity(data, match));
} catch (err) {
console.error('[crm-sync]', data.callHistoryId, err); // 재처리 대기열에 넣는다
}
});
});
app.listen(process.env.PORT || 3000);
활동을 externalId로 upsert하므로 같은 웹훅이 두 번 와도 활동은 하나만 남습니다. 다만 고객이 없어 리드를 만드는 경로는 두 요청이 동시에 들어오면 리드가 둘 생길 수 있습니다. 리드 생성도 정규화한 번호를 키로 "있으면 재사용"하게 만들거나, 같은 번호의 처리를 잠깐 잠그는 방식으로 막습니다.
녹음, 누락 보정, 개인정보 다루기
통화 기록 연동을 운영하면서 자주 부딪히는 세 가지를 정리합니다.
- 녹음 파일:
recordingUrl을 CRM에 링크로만 남기면 나중에 열리지 않을 수 있습니다. 통화 기록 API의 녹음 URL은 1시간 뒤 만료되므로, 보관이 필요하면 받은 즉시 내려받아 CRM 첨부나 사내 저장소에 둡니다. 방법은 통화 녹음 파일 내려받기 글을 참고합니다. - 누락 보정: 연동 서버가 내려가 있던 동안의 통화는
GET /call-history에updatedAfter를 넣어 다시 가져옵니다. 목록 응답의 각 항목에도fromNumber,toNumber,isInbound,summary,sttAI,sttDirect가 있으므로 같은 매칭 함수를 그대로 쓸 수 있고, 녹음은recordingAI,recordingDirect로 나뉘어 옵니다. 커서 사용법은 통화 기록 증분 동기화 글에 있습니다. - 개인정보: 대화 내역에는 고객이 말한 개인정보가 그대로 담깁니다. CRM에서 대화 내역 필드를 볼 수 있는 역할을 제한하고, 보관 기간을 정해 지웁니다. 일반 원칙은 AI 전화와 개인정보 글에 정리했으며, 실제 적용은 법률 검토가 필요합니다.
특정 통화를 다시 확인할 때는 상세 조회를 씁니다. groupId는 필수 쿼리 파라미터입니다.
curl -s "https://studio-gateway.zerocall.kr/call-history/8f3f8fd9-7ab8-4a26-b523-4f8eb7d38b85?groupId=1" \
-H "x-api-key: $KAT_API_KEY"통화가 들어오는 순간 상담원 화면에 고객 정보를 띄우고 싶다면 CALL_RECEIVED 웹훅을 함께 씁니다. 이 이벤트에는 fromNumber, 연락처에 등록된 경우의 fromName, AI에서 직원으로 넘어올 때의 handOffReason이 들어 있어, 같은 정규화·매칭 함수로 CRM 고객 화면을 미리 찾아 둘 수 있습니다. 상세 필드는 통화 기록 API 문서와 웹훅 이벤트 문서에 있습니다.
자주 묻는 질문
발신 통화에서는 어느 번호로 고객을 찾나요?
KAT Cloud CALL_COMPLETED의 isInbound가 false인 발신 통화에서는 toNumber가 고객 번호이고 fromNumber는 내 KAT Cloud 번호입니다. 수신 통화는 반대로 fromNumber가 고객 번호이므로, 방향을 확인하지 않고 한쪽 필드만 쓰면 발신 통화가 모두 내 회사 번호로 매칭되는 문제가 생깁니다.
CRM에 같은 통화가 두 번 기록되지 않게 하려면 어떻게 하나요?
callHistoryId로 만든 값을 CRM 활동의 외부 ID로 저장하고, 기록할 때 같은 외부 ID가 있으면 갱신하는 upsert 방식을 쓰면 같은 통화가 두 번 기록되지 않습니다. 이 방식은 웹훅과 통화 기록 API의 누락 보정 조회를 함께 쓸 때도 그대로 통합니다.
CRM에 전화번호가 하이픈이나 +82 형식으로 저장되어 있으면 매칭이 안 되나요?
KAT Cloud 웹훅의 전화번호는 숫자만 담긴 국내 형식(예: 01012345678)이므로, CRM 번호가 010-1234-5678이나 +82 10-1234-5678처럼 저장되어 있으면 그대로는 일치하지 않습니다. 양쪽 번호에서 숫자만 남기고 국가번호 82를 0으로 바꾸는 정규화를 거친 뒤 비교하면 같은 번호로 매칭됩니다.
- #CRM 연동
- #통화 기록
- #CALL_COMPLETED
- #고객 매칭



