KAT Cloud로그인

통화 요약을 슬랙으로 보내기: CALL_COMPLETED 웹훅 활용

KAT Cloud의 CALL_COMPLETED 웹훅을 받아 통화 요약, 발신 번호, 통화 시간, 응대 태그를 슬랙 Incoming Webhook 메시지로 보내는 방법입니다. 블록 구성, 번호 마스킹, 부재중 통화 분리까지 Node.js 코드로 설명합니다.

가이드KAT Cloud 팀8분 읽기
통화 요약을 슬랙으로 보내기: CALL_COMPLETED 웹훅 활용

통화 요약을 슬랙으로 보내려면 KAT Cloud의 CALL_COMPLETED 웹훅을 받는 작은 서버를 두고, 받은 요약(summary), 발신 번호, 통화 시간(duration), 응대 태그(tag)를 슬랙 Incoming Webhook URL로 POST하면 됩니다. CALL_COMPLETED는 통화가 끝나고 STT·요약 분석이 완료된 뒤 전송되므로, 메시지 하나로 어떤 전화가 왔고 어떻게 응대됐는지 팀이 바로 확인할 수 있습니다.

구성 한눈에 보기

KAT Cloud(캣클라우드)와 슬랙 사이에는 중계 서버가 하나 필요합니다. KAT Cloud가 보내는 웹훅 본문과 슬랙이 받는 메시지 형식이 다르기 때문입니다. 중계 서버는 CALL_COMPLETED를 받아 즉시 200으로 응답하고, 필요한 필드만 골라 슬랙 메시지로 바꿔 보냅니다.

고객 전화가 KAT Cloud에서 STT와 요약 분석을 거친 뒤 CALL_COMPLETED 웹훅으로 내 서버에 전달되고, 서버가 슬랙 Incoming 웹훅으로 메시지를 보내는 흐름도
중계 서버가 웹훅 본문을 슬랙 메시지 형식으로 바꿉니다.

준비물은 다음과 같습니다.

  • KAT Cloud 그룹: 웹훅 관리 API는 비즈니스 요금제 그룹 또는 별도 허용된 그룹에서 쓸 수 있습니다.
  • 슬랙 Incoming Webhook URL: 슬랙 앱에서 Incoming Webhooks를 켜고 알림 받을 채널을 고르면 https://hooks.slack.com/services/... 형태의 URL이 발급됩니다.
  • Node.js 18 이상과 Express, 그리고 https로 접근 가능한 서버 주소.

슬랙 메시지에 넣을 항목과 CALL_COMPLETED 필드의 대응은 다음과 같습니다.

슬랙에 보일 항목CALL_COMPLETED 필드가공 방법
요약summary비어 있으면 "요약 없음"
발신자isInbound가 true면 fromNumber가운데 자리 마스킹
수신자(발신 통화일 때)isInbound가 false면 toNumber가운데 자리 마스킹
통화 시간duration(초)"1분 36초" 형식
응대 태그tagAI 응대, 직원 응대, AI→직원 연결, 확인 불가
통화 상태status응답, 부재중, 거절, 확인 불가
시작 시각startedAt한국 시간으로 표기
기록 IDcallHistoryId상세 조회와 중복 확인에 사용

1단계: 슬랙 Incoming Webhook 준비

슬랙 Incoming Webhook URL은 그 자체가 비밀 값입니다. URL만 알면 누구나 그 채널에 글을 올릴 수 있으므로 코드에 적지 말고 환경 변수로 넣습니다. 발급받았다면 cURL로 먼저 메시지가 올라가는지 확인합니다.

Shell
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T000/B000/XXXX"

curl -s -X POST "$SLACK_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d '{"text":"KAT Cloud 통화 알림 연동 테스트"}'

부재중 통화를 다른 채널로 받고 싶다면 그 채널용 Incoming Webhook URL을 하나 더 만들어 SLACK_MISSED_WEBHOOK_URL로 둡니다. Incoming Webhook은 URL 하나가 채널 하나에 묶여 있기 때문입니다.

2단계: CALL_COMPLETED를 슬랙 메시지로 바꾸기

메시지 형식은 슬랙 Block Kit을 씁니다. 머리글에 통화 방향·태그·상태를 넣고, 필드 영역에 번호·통화 시간·태그·시작 시각을, 본문에 요약을 둡니다. 알림 미리보기에 쓰이는 text도 함께 넣습니다.

JavaScript
// slack-format.js
const TAG_LABEL = { AI: 'AI 응대', DIRECT: '직원 응대', AI_TO_DIRECT: 'AI→직원 연결', UNKNOWN: '확인 불가' };
const STATUS_LABEL = { ANSWERED: '응답', MISSED: '부재중', REJECTED: '거절', UNKNOWN: '확인 불가' };

function formatDuration(sec = 0) {
  const s = Math.max(0, Math.round(sec));
  const m = Math.floor(s / 60);
  return m > 0 ? `${m}분 ${s % 60}초` : `${s}초`;
}

function maskPhone(num = '') {
  const d = String(num).replace(/\D/g, '');
  if (d.length < 8) return d || '번호 없음';
  return `${d.slice(0, 3)}-****-${d.slice(-4)}`; // 01012345678 → 010-****-5678
}

function toKst(iso) {
  if (!iso) return '-';
  return new Intl.DateTimeFormat('ko-KR', {
    timeZone: 'Asia/Seoul', month: 'numeric', day: 'numeric',
    hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
  }).format(new Date(iso));
}

// 슬랙 mrkdwn에서 특수하게 쓰이는 문자 세 개를 이스케이프
const escape = (t = '') => t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

function buildSlackMessage(d) {
  const tag = TAG_LABEL[d.tag] || '확인 불가';
  const status = STATUS_LABEL[d.status] || '확인 불가';
  const direction = d.isInbound ? '수신' : '발신';
  const who = d.isInbound ? '발신자' : '수신자';
  const number = d.isInbound ? d.fromNumber : d.toNumber;
  const summary = escape(d.summary || '요약 없음').slice(0, 2900); // section 텍스트 한도 3,000자
  const title = `${direction} 통화 · ${tag} · ${status}`;

  return {
    text: `${title} - ${summary.slice(0, 80)}`,
    blocks: [
      { type: 'header', text: { type: 'plain_text', text: title } },
      {
        type: 'section',
        fields: [
          { type: 'mrkdwn', text: `*${who}*\n${maskPhone(number)}` },
          { type: 'mrkdwn', text: `*통화 시간*\n${formatDuration(d.duration)}` },
          { type: 'mrkdwn', text: `*응대 태그*\n${tag}` },
          { type: 'mrkdwn', text: `*시작 시각*\n${toKst(d.startedAt)}` },
        ],
      },
      { type: 'section', text: { type: 'mrkdwn', text: `*요약*\n${summary}` } },
      { type: 'context', elements: [{ type: 'mrkdwn', text: `callHistoryId: ${d.callHistoryId}` }] },
    ],
  };
}

module.exports = { buildSlackMessage };

수신 통화(isInbound: true)에서는 fromNumber가 고객이고, 발신 통화에서는 toNumber가 고객입니다. 그래서 방향에 따라 "발신자" 또는 "수신자"로 라벨을 바꿔 보여 줍니다. 태그 값의 의미, 특히 AI_TO_DIRECT가 어떤 통화인지는 AI에서 사람으로: 상담원 연결 설계 글에서 설명합니다.

웹훅을 받아 즉시 응답하고, callHistoryId로 중복을 확인하고, 슬랙 메시지를 만들고, 상태에 따라 채널을 골라 전송하는 네 단계
각 단계는 실패해도 다음 웹훅 처리를 막지 않게 만듭니다.

3단계: 웹훅 서버에서 슬랙으로 전송

서버는 CALL_COMPLETED만 처리합니다. 먼저 200을 돌려주고, 같은 callHistoryId로 이미 보낸 통화인지 확인한 뒤 슬랙으로 보냅니다. 슬랙이 429(요청 과다)나 5xx를 돌려주면 Retry-After 헤더만큼 기다렸다가 다시 시도합니다.

JavaScript
// server.js — Node.js 18+, Express 4
const express = require('express');
const { buildSlackMessage } = require('./slack-format');

const SLACK_DEFAULT = process.env.SLACK_WEBHOOK_URL;
const SLACK_MISSED = process.env.SLACK_MISSED_WEBHOOK_URL || SLACK_DEFAULT;
const posted = new Set(); // 보낸 callHistoryId (운영에서는 Redis·DB)

async function postToSlack(url, message) {
  for (let attempt = 1; attempt <= 3; attempt++) {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(message),
    });
    if (res.ok) return;
    if (res.status === 429 || res.status >= 500) {
      const waitSec = Number(res.headers.get('retry-after')) || attempt * 2;
      await new Promise((r) => setTimeout(r, waitSec * 1000));
      continue;
    }
    throw new Error(`Slack ${res.status} ${await res.text()}`);
  }
  throw new Error('Slack 전송 재시도 초과');
}

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;
  if (posted.has(data.callHistoryId)) return;
  posted.add(data.callHistoryId);

  const url = data.status === 'MISSED' ? SLACK_MISSED : SLACK_DEFAULT;
  postToSlack(url, buildSlackMessage(data)).catch((err) => {
    posted.delete(data.callHistoryId); // 다음 재전송 때 다시 시도할 수 있게
    console.error('[slack]', data.callHistoryId, err);
  });
});

app.listen(process.env.PORT || 3000);

서버를 띄운 뒤에는 KAT Cloud에 CALL_COMPLETED 웹훅을 등록하고, 테스트 엔드포인트로 연결을 확인합니다.

Shell
export KAT_API_KEY="콘솔에서 발급받은 키"
BASE="https://studio-gateway.zerocall.kr"

curl -s -X POST "$BASE/webhook" \
  -H "x-api-key: $KAT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"eventType":"CALL_COMPLETED","url":"https://hooks.example.com/webhook/call-completed"}'

# 응답의 id로 테스트 페이로드 전송 → success, statusCode, responseTime, error 확인
curl -s -X POST "$BASE/webhook/1/test" -H "x-api-key: $KAT_API_KEY"

실제 통화 없이 슬랙 메시지 모양을 보려면 문서의 예시 페이로드를 로컬 서버에 직접 보냅니다.

Shell
curl -s -X POST http://localhost:3000/webhook/call-completed \
  -H "Content-Type: application/json" \
  -d '{"eventType":"CALL_COMPLETED","timestamp":"2026-03-12T02:45:30.000Z","data":{"callHistoryId":"8f3f8fd9-7ab8-4a26-b523-4f8eb7d38b85","groupId":1,"channelId":100,"fromNumber":"01012345678","toNumber":"07012345678","isInbound":true,"status":"ANSWERED","tag":"AI_TO_DIRECT","duration":96,"summary":"예약 변경 요청을 접수하고 변경 가능한 시간대를 안내함","sttAI":[],"sttDirect":[],"startedAt":"2026-03-12T02:40:00.000Z","completedAt":"2026-03-12T02:41:36.000Z"}}'

웹훅 이벤트 필드 전체는 API 문서의 웹훅 이벤트 항목에 있고, 여러 이벤트를 한 서버로 받는 구조는 Node.js 웹훅 수신 서버 만들기를 참고합니다.

메시지를 더 쓸모 있게 만드는 팁

알림이 쌓이기 시작하면 무엇을 어느 채널에 얼마나 보낼지가 중요해집니다.

  • 채널 나누기: 부재중(MISSED)은 콜백 담당 채널로, AI_TO_DIRECT는 상담 품질을 보는 채널로 보내면 각 팀이 자기 일만 봅니다.
  • 요약 길이: summary는 보통 한두 문장이지만 길어질 수 있습니다. 슬랙 section 텍스트는 3,000자, header는 150자까지라 잘라서 넣습니다.
  • 번호 마스킹: 슬랙 채널은 고객 정보를 다룰 필요가 없는 사람도 볼 수 있습니다. 전체 번호는 사내 시스템에서 callHistoryId로 조회하게 하고 슬랙에는 가린 번호만 둡니다.
  • 대화 전문은 올리지 않기: sttAI, sttDirect 전문이나 녹음 URL을 슬랙에 그대로 올리면 개인정보가 넓게 퍼집니다. 필요한 사람만 원문을 볼 수 있게 링크 대신 기록 ID를 남깁니다.
  • 요약이 비는 통화: 녹음이 꺼진 통화는 요약과 STT가 제공되지 않습니다. "요약 없음"으로 표시하되 상태와 통화 시간은 그대로 보여 줍니다.

통화 데이터 보관과 고지에 관한 일반 원칙은 AI 전화와 개인정보 글에 정리했으며, 실제 적용은 법률 검토가 필요합니다.

자주 묻는 질문

통화가 끝나자마자 슬랙 알림이 오나요?

KAT Cloud의 CALL_COMPLETED는 통화 종료 후 STT와 요약 분석이 완료되면 전송되므로, 슬랙 알림도 분석이 끝난 시점에 옵니다. 전화가 걸려 오는 순간 알림을 받고 싶다면 CALL_RECEIVED 웹훅을 따로 등록해 발신 번호와 착신 알림을 먼저 보내는 구성을 함께 씁니다.

요약이 비어 있는 통화는 왜 생기나요?

KAT Cloud는 통화 당시 녹음이 활성화되지 않았다면 요약(summary)과 STT 대화 내역을 제공하지 않습니다. 이런 통화도 상태, 태그, 통화 시간은 알 수 있으므로 슬랙 메시지에서는 요약 자리에 "요약 없음"을 표시하고 나머지 항목은 그대로 보여 주는 것이 좋습니다.

대화 전문도 슬랙에 올려도 되나요?

통화 대화 전문에는 이름, 연락처, 예약 내용 같은 개인정보가 들어갈 수 있어 슬랙처럼 여러 사람이 보는 채널에 올리는 것은 권하지 않습니다. 슬랙에는 KAT Cloud가 만든 요약과 callHistoryId만 두고, 전문은 접근 권한이 관리되는 사내 시스템에서 확인하게 하는 편이 안전합니다.

  • #슬랙
  • #CALL_COMPLETED
  • #통화 요약
  • #웹훅

Get started

만든 AI에게 전화번호를 주세요.

070 번호 발급, SIP 직접 연결, 음성 AI 모델 연결, 문자·웹훅까지 API 하나로. 월 10,000원부터.