KAT Cloud로그인

문자 발송 결과 확인: SEND_RESULT 웹훅과 성공 코드

KAT Cloud 문자의 최종 전달 여부는 resultStatus가 4100(SMS), 6600(LMS·MMS), 7000(카카오) 중 하나인지로 판단합니다. SEND_RESULT 웹훅, 결과 조회 API, sendStatus와의 차이를 정리했습니다.

가이드KAT Cloud 팀9분 읽기
문자 발송 결과 확인: SEND_RESULT 웹훅과 성공 코드

KAT Cloud에서 문자가 실제로 고객에게 전달됐는지는 발송 결과의 resultStatus 코드로 판단합니다. SMS는 4100, LMS·MMS는 6600, 카카오 메시지는 7000이 성공(전달) 코드이고, 이 결과는 SEND_RESULT 웹훅으로 받거나 GET /message/send-result/{chatId}로 조회합니다. 발송 요청이 성공했다는 응답이나 sendStatus: SUCCESS만으로는 전달이 끝났다고 볼 수 없습니다.

발송 결과가 확정되는 순서

KAT Cloud(캣클라우드)의 문자 발송은 즉시 응답과 최종 결과가 시간차를 두고 나뉩니다. POST /message/send는 곧바로 code: "0", message: "Success"와 함께 result에 32자 chatId를 돌려줍니다. 이 시점은 요청을 접수한 단계입니다. 이후 KAT Cloud가 메시지 사업자에게 요청을 넘기고, 메시지 사업자가 최종 발송 결과를 회신하면 그때 SEND_RESULT 웹훅이 전송됩니다. 채팅 API의 POST /chat도 마찬가지로 응답에 chatId가 있고, sendStatus는 PENDING에서 시작합니다.

내 서버가 문자 발송을 요청하면 KAT Cloud가 chatId를 즉시 돌려주고, 메시지 사업자에게 요청을 넘긴 뒤 최종 결과를 회신받아 SEND_RESULT 웹훅으로 알려 주는 순서도
즉시 응답은 접수 확인이고, 전달 여부는 나중에 오는 resultStatus로 판단합니다.

sendStatus와 resultStatus는 무엇이 다른가

두 필드는 이름이 비슷하지만 가리키는 단계가 다릅니다. 또 sendStatus는 어느 API에서 보느냐에 따라 값의 종류가 다릅니다.

필드보이는 곳값의미
sendStatusSEND_RESULT 웹훅, GET /message/send-result/{chatId}SUCCESS, FAILEDKAT Cloud가 메시지 사업자에게 요청을 넘긴 상태
sendStatus채팅 API(POST /chat, GET /chat)RECEIVED, PENDING, SENT, FAILED채팅 메시지의 발송 상태. RECEIVED는 고객이 보낸 메시지
resultStatus위 모든 곳결과 코드 문자열최종 전달 결과 코드
resultMessage위 모든 곳한글 메시지결과 코드의 설명(예: 전달)
chatType결과 웹훅, 결과 조회SMS, LMS, MMS, RCS, KAKAO메시지 타입

sendStatus: SUCCESS는 "메시지 사업자에게 넘기는 데 성공했다"는 뜻일 뿐입니다. 수신 번호가 없거나 단말이 받지 못하는 등의 이유로 최종 전달이 실패할 수 있고, 그 결과는 resultStatus에 담깁니다. 반대로 sendStatus: FAILED라면 메시지 사업자에게 넘기는 단계에서 이미 실패한 것입니다.

최종 성공 코드는 다음 세 개입니다.

구분성공 코드resultMessage
SMS4100전달
LMS·MMS6600전달
카카오7000전달

이 세 코드가 아닌 resultStatus는 전달 실패로 봅니다. 실패 코드별 의미는 API 문서의 결과 코드 표에 SMS, LMS·MMS, 카카오별로 나뉘어 있습니다.

sendStatus는 메시지 사업자에게 요청을 넘겼는지를, resultStatus는 최종 전달 결과 코드를 나타낸다는 차이를 비교한 표
전달 성공 판정은 resultStatus 하나로 합니다.

방법 1: SEND_RESULT 웹훅으로 받기

결과를 빠르고 빠짐없이 받으려면 웹훅이 가장 낫습니다. POST /webhook에 eventType: SEND_RESULT와 https URL을 등록합니다. 웹훅 관리 API는 비즈니스 요금제 그룹 또는 별도 허용된 그룹에서 쓸 수 있습니다.

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":"SEND_RESULT","url":"https://hooks.example.com/webhook/send-result"}'

웹훅 본문의 data.chatId는 발송할 때 받은 32자 chatId와 같습니다. 그래서 발송 시점에 chatId를 내 DB에 저장해 두고, 웹훅이 오면 그 행을 갱신하는 구조가 기본입니다.

JSON
{
  "eventType": "SEND_RESULT",
  "timestamp": "2024-03-05T10:30:05.000Z",
  "data": {
    "chatId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "chatRoomId": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "groupId": 1,
    "channelId": 100,
    "sendPhoneNumber": "07012345678",
    "recvPhoneNumber": "01098765432",
    "content": "예약이 확정되었습니다.",
    "chatType": "SMS",
    "sendStatus": "SUCCESS",
    "resultStatus": "4100",
    "resultMessage": "전달",
    "sentAt": "2024-03-05T10:30:05.000Z"
  }
}

Node.js(Express)로 받는 예시입니다. 결과 판정은 함수 하나로 모아 두고, 저장은 chatId 기준 UPDATE로 합니다. 같은 웹훅이 두 번 와도 같은 값으로 덮어쓰기 때문에 결과가 달라지지 않습니다.

JavaScript
// send-result-webhook.js — Node.js 18+, Express 4
const express = require('express');

const SUCCESS_CODES = new Set(['4100', '6600', '7000']); // SMS, LMS·MMS, 카카오

function classify({ sendStatus, resultStatus }) {
  if (sendStatus === 'FAILED') return 'REQUEST_FAILED'; // 사업자 요청 단계에서 실패
  if (resultStatus == null || resultStatus === '') return 'PENDING';
  return SUCCESS_CODES.has(String(resultStatus)) ? 'DELIVERED' : 'UNDELIVERED';
}

// 내 DB에 맞게 구현하는 부분
const messages = {
  async updateByChatId(chatId, patch) {
    // UPDATE messages SET outcome = ?, result_status = ?, ... WHERE chat_id = ?
  },
};

const app = express();
app.use(express.json());

app.post('/webhook/send-result', (req, res) => {
  res.sendStatus(200); // 먼저 응답 (타임아웃 10초)

  const { eventType, data } = req.body || {};
  if (eventType !== 'SEND_RESULT' || !data?.chatId) return;

  setImmediate(async () => {
    try {
      await messages.updateByChatId(data.chatId, {
        outcome: classify(data),
        chatType: data.chatType,
        sendStatus: data.sendStatus,
        resultStatus: data.resultStatus,
        resultMessage: data.resultMessage,
        sentAt: data.sentAt,
      });
    } catch (err) {
      console.error('[send-result] 저장 실패', data.chatId, err);
    }
  });
});

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

여러 이벤트를 한 서버에서 받는 구조와 중복 제거 방법은 Node.js 웹훅 수신 서버 만들기에서 다룹니다.

방법 2: 결과 조회 API로 확인하기

웹훅을 쓰지 않거나, 웹훅이 오기 전에 상태를 직접 확인해야 할 때는 GET /message/send-result/{chatId}를 씁니다. 이 API는 SEND_RESULT 웹훅과 같은 최종 결과 정보를 조회용으로 제공하며, 문자 발신 API 범위에 속해 모든 사용자가 쓸 수 있습니다. 문서상 조회 대상은 /message/send가 돌려준 32자 chatId입니다.

Shell
curl -s "$BASE/message/send-result/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "x-api-key: $KAT_API_KEY"
JSON
{
  "code": "0",
  "message": "Success",
  "result": {
    "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "content": "안녕하세요. 예약이 확정되었습니다.",
    "subject": "예약 확정 안내",
    "imageList": [],
    "externalMessageId": "biz_1234567890",
    "fromPhoneNumber": "07012345678",
    "toPhoneNumber": "01098765432",
    "sendStatus": "SUCCESS",
    "chatType": "SMS",
    "resultStatus": "4100",
    "resultMessage": "전달",
    "createdAt": "2024-03-05T10:30:05.000Z"
  }
}

최종 결과가 아직 없을 수 있으므로 조회는 간격을 늘려 가며 몇 번만 시도하고, 그래도 없으면 대기 상태로 남겨 둡니다. Python 예시입니다.

Python
import os
import time

import requests

BASE_URL = "https://studio-gateway.zerocall.kr"
HEADERS = {"x-api-key": os.environ["KAT_API_KEY"]}
SUCCESS_CODES = {"4100", "6600", "7000"}


def get_send_result(chat_id: str) -> dict:
    r = requests.get(f"{BASE_URL}/message/send-result/{chat_id}", headers=HEADERS, timeout=10)
    r.raise_for_status()
    return r.json()["result"]


def wait_for_result(chat_id: str, attempts: int = 6, first_delay: float = 5.0) -> dict:
    delay = first_delay
    for _ in range(attempts):
        result = get_send_result(chat_id)
        if result.get("sendStatus") == "FAILED":
            return {"outcome": "REQUEST_FAILED", **result}
        code = result.get("resultStatus")
        if code:
            outcome = "DELIVERED" if str(code) in SUCCESS_CODES else "UNDELIVERED"
            return {"outcome": outcome, **result}
        time.sleep(delay)
        delay = min(delay * 2, 60)  # 5초, 10초, 20초 ... 최대 60초
    return {"outcome": "PENDING", "id": chat_id}

채팅 API로 보낸 메시지는 GET /chat?chatRoomId=...의 대화 조회 결과에 들어 있는 sendStatus, resultStatus, chatType으로도 상태를 확인할 수 있습니다. 대량 발송 건을 하나씩 조회하면 Rate Limit(60초당 1,200회)에 금방 닿으므로, 조회는 웹훅을 놓친 건을 보정하는 용도로 쓰는 것이 좋습니다.

결과 처리 설계 팁

발송 결과를 다룰 때는 요청 단계의 실패와 전달 단계의 실패를 나눠서 기록해야 원인을 찾기 쉽습니다. /message/send 요청 자체가 실패하면 결과 코드가 아니라 HTTP 에러가 돌아옵니다.

HTTP코드의미대응
400Validation Error전화번호 형식, 본문·제목 길이, imageList 항목 길이 등 요청 형식 오류요청을 고친 뒤 다시 보냄
404CH001발신에 사용할 채널을 찾을 수 없음fromPhoneNumber와 그룹 번호 확인
503CH002문자 발송 실패잠시 뒤 재시도

그 밖에 챙길 점은 다음과 같습니다.

  • chatId는 발송 응답을 받는 즉시 저장합니다. 웹훅이 먼저 도착해도 맞춰 볼 수 있게, 저장 전 도착한 결과는 잠시 보관했다가 다시 매칭합니다.
  • resultStatus가 비어 있으면 실패가 아니라 대기입니다. 일정 시간이 지나도 결과가 없을 때만 조회 API로 확인합니다.
  • 전달 실패를 자동으로 재발송하지 않습니다. 같은 번호로 다시 보내도 같은 이유로 실패할 수 있으니 resultMessage로 사유를 보고 판단합니다.
  • 일·주 단위로 chatType별 전달률을 집계하면 번호 데이터 품질이나 이미지 규격 문제를 일찍 발견할 수 있습니다.
  • 429나 503 같은 일시적 오류의 재시도 간격은 API 에러 처리와 Rate Limit 대응 글의 백오프 방식을 따릅니다.

채팅방 기반 발송 방법은 채팅 API로 문자 보내기에, 웹훅 이벤트 필드 전체는 웹훅 이벤트 문서에 있습니다.

자주 묻는 질문

sendStatus가 SUCCESS인데 문자가 오지 않았습니다. 왜 그런가요?

KAT Cloud 발송 결과의 sendStatus: SUCCESS는 KAT Cloud가 메시지 사업자에게 요청을 넘겼다는 뜻이지, 고객 휴대폰에 전달됐다는 뜻이 아닙니다. 실제 전달 여부는 resultStatus로 판단하며, 이 값이 4100, 6600, 7000 중 하나가 아니면 전달에 실패한 것이므로 resultMessage로 사유를 확인합니다.

결과 코드 4100, 6600, 7000은 각각 무엇인가요?

KAT Cloud 문서 기준으로 4100은 SMS, 6600은 LMS와 MMS, 7000은 카카오 메시지의 전달 성공 코드이며, 세 코드 모두 resultMessage는 "전달"입니다. 문자 종류가 달라도 성공 판정은 "세 코드 중 하나인가"라는 규칙 하나로 처리할 수 있습니다.

웹훅 없이 발송 결과를 확인할 수 있나요?

KAT Cloud는 GET /message/send-result/{chatId}로 SEND_RESULT 웹훅과 같은 최종 결과를 조회할 수 있게 제공하며, 이 API는 모든 사용자가 쓸 수 있습니다. 다만 결과가 확정되기 전에는 resultStatus가 비어 있을 수 있으므로 간격을 두고 몇 번 조회하고, 발송량이 많다면 웹훅을 기본으로 쓰는 편이 효율적입니다.

  • #발송 결과
  • #SEND_RESULT
  • #결과 코드
  • #문자 발송

Get started

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

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