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

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에서 시작합니다.

sendStatus와 resultStatus는 무엇이 다른가
두 필드는 이름이 비슷하지만 가리키는 단계가 다릅니다. 또 sendStatus는 어느 API에서 보느냐에 따라 값의 종류가 다릅니다.
| 필드 | 보이는 곳 | 값 | 의미 |
|---|---|---|---|
sendStatus | SEND_RESULT 웹훅, GET /message/send-result/{chatId} | SUCCESS, FAILED | KAT 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 |
|---|---|---|
| SMS | 4100 | 전달 |
| LMS·MMS | 6600 | 전달 |
| 카카오 | 7000 | 전달 |
이 세 코드가 아닌 resultStatus는 전달 실패로 봅니다. 실패 코드별 의미는 API 문서의 결과 코드 표에 SMS, LMS·MMS, 카카오별로 나뉘어 있습니다.

방법 1: SEND_RESULT 웹훅으로 받기
결과를 빠르고 빠짐없이 받으려면 웹훅이 가장 낫습니다. POST /webhook에 eventType: SEND_RESULT와 https URL을 등록합니다. 웹훅 관리 API는 비즈니스 요금제 그룹 또는 별도 허용된 그룹에서 쓸 수 있습니다.
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에 저장해 두고, 웹훅이 오면 그 행을 갱신하는 구조가 기본입니다.
{
"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로 합니다. 같은 웹훅이 두 번 와도 같은 값으로 덮어쓰기 때문에 결과가 달라지지 않습니다.
// 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입니다.
curl -s "$BASE/message/send-result/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "x-api-key: $KAT_API_KEY"{
"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 예시입니다.
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 | 코드 | 의미 | 대응 |
|---|---|---|---|
| 400 | Validation Error | 전화번호 형식, 본문·제목 길이, imageList 항목 길이 등 요청 형식 오류 | 요청을 고친 뒤 다시 보냄 |
| 404 | CH001 | 발신에 사용할 채널을 찾을 수 없음 | fromPhoneNumber와 그룹 번호 확인 |
| 503 | CH002 | 문자 발송 실패 | 잠시 뒤 재시도 |
그 밖에 챙길 점은 다음과 같습니다.
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
- #결과 코드
- #문자 발송



