결과 Webhook
발송 결과가 확정되면 레드빈8 서버가 고객사가 등록한 URL 로 직접 HTTPS POST 합니다. 폴링 없이 결과를 실시간으로 반영할 수 있습니다.
설정
- 결과 Webhook 메뉴에서 수신 URL 을 등록합니다.
- 발급된 서명 시크릿(
whsec_…)을 안전한 곳에 저장합니다. (원문은 발급 직후 한 번만 표시) - 테스트 전송 버튼으로 연결을 확인합니다. (
webhook.test이벤트)
수신 URL 조건
https://만 가능하며 포트는 443 입니다. 유효한 TLS 인증서가 필요합니다.- IP 주소 직접 지정, 사설망/내부망으로 해석되는 도메인, 사용자 정보(
user:pass@)가 포함된 URL 은 등록할 수 없습니다. - 리다이렉트(3xx)는 따라가지 않습니다. 최종 URL 을 등록하세요.
어떤 이벤트가 전송되나요?
| event | 시점 |
|---|---|
message.delivered | 수신자에게 전달 성공으로 확정 |
message.failed | 전달 실패로 확정 (요금 환불 완료) |
message.rejected | 발송 대행 시스템이 접수 자체를 거절 (요금 환불 완료) |
message.cancelled | 예약 취소로 확정 (요금 환불 완료) |
- 최종 상태만 전송합니다. 중간 상태(
SUBMITTED등)는 보내지 않습니다. - API 로 접수한 발송 건만 대상입니다. 웹 화면에서 발송한 건은 전송되지 않습니다.
- 수신자 1명당 최종 이벤트는 1회만 생성됩니다.
요청 형식
이벤트는 성능을 위해 최대 100건씩 묶어서 한 번의 POST 로 전송합니다.
헤더
| 헤더 | 설명 |
|---|---|
Content-Type | application/json |
X-Redbean-Event | message.batch (테스트는 webhook.test) |
X-Redbean-Delivery-Id | 전송 시도 단위 ID (본문 id 와 동일). 재시도하면 새 값이 부여되므로 중복 처리 방지에는 event + recipient_id 를 사용하세요. 로그 추적용으로 활용합니다. |
X-Redbean-Timestamp | 전송 시각 (Unix epoch 초). 재시도마다 갱신됩니다. |
X-Redbean-Signature | v1= + HMAC-SHA256 16진수 (아래 참고) |
User-Agent | Redbean-Webhook/1.0 |
본문
JSON
{
"id": "dlv_75ca12ec-9e17-456a-8ee2-f770b512ed4f",
"type": "message.batch",
"created_at": "2026-11-07T09:00:06+09:00",
"events": [
{
"event": "message.delivered",
"message_id": 1374,
"recipient_id": 3304886,
"channel": "sms",
"to": "01011112222",
"status": "DELIVERED",
"client_ref": "A-1001",
"request_client_ref": "ship-20261107-am",
"result": { "code": "4100", "message": "전달", "carrier": "SKT" },
"delivered_at": "2026-11-07T09:00:05+09:00"
},
{
"event": "message.failed",
"message_id": 1374,
"recipient_id": 3304887,
"channel": "sms",
"to": "01033334444",
"status": "FAILED",
"client_ref": "A-1002",
"request_client_ref": "ship-20261107-am",
"result": { "code": "4410", "message": "잘못된 번호", "carrier": null },
"delivered_at": null
}
]
}| 필드 | 설명 |
|---|---|
events[].message_id | 발송 건 ID (발송 응답의 message_id) |
events[].recipient_id | 수신자 ID. 멱등 처리 키로 사용하세요. (event + recipient_id) |
events[].client_ref | 수신자별 참조키 (recipients[].ref) |
events[].request_client_ref | 요청 참조키 (client_ref) |
events[].status | 수신자 최종 상태: DELIVERED · FAILED · REJECTED · CANCELLED |
events[].result.code | 결과 코드 (아래 결과 코드표). 취소는 null, 접수 거절은 REJECTED |
events[].result.message | 결과 설명(한글). 코드표의 설명과 같습니다. |
events[].result.carrier | 수신 통신사 (확인된 경우) |
events[].delivered_at | 단말 도달 시각 (성공 시) |
결과 코드
events[].status 와 result.code 의 관계입니다. 조회 API(/v1/messages/{id}/recipients)의 result 도 같은 값을 사용합니다.
| status | event | result.code | 요금 |
|---|---|---|---|
DELIVERED | message.delivered | 성공 코드 (SMS 4100, LMS/MMS 6600 등 — 아래 표의 DELIVERED) | 차감 유지 |
FAILED | message.failed | 실패 코드 (아래 표의 FAILED). 표에 없는 코드는 message 가 “발송에 실패했습니다.” | 자동 환불 |
REJECTED | message.rejected | REJECTED (고정). 발송 대행 시스템 내부 사유는 공개하지 않습니다. | 자동 환불 |
CANCELLED | message.cancelled | null | 자동 환불 |
채널별 상세 결과 코드는 아래와 같습니다. 코드는 통신사·카카오 정책에 따라 추가될 수 있으므로, 모르는 코드는 status 로 성공/실패를 판단하고 message 를 그대로 표시하세요.
SMS 결과 코드 (32)
| code | status | 설명 |
|---|---|---|
4100 | DELIVERED | 전달 |
4400 | FAILED | 음영 지역 |
4401 | FAILED | 단말기 전원 꺼짐 |
4402 | FAILED | 단말기 메시지 저장 초과 |
4403 | FAILED | 메시지 삭제 됨 |
4404 | FAILED | 가입자 위치 정보 없음 |
4405 | FAILED | 단말기 BUSY |
4410 | FAILED | 잘못된 번호 |
4411 | FAILED | NPDB 에러 |
4412 | FAILED | 착신거절 |
4413 | FAILED | SMSC 형식오류 |
4414 | FAILED | 비가입자, 결번, 서비스정지 |
4415 | FAILED | 식별코드 오류 |
4420 | FAILED | 기타에러 |
4421 | FAILED | 타임아웃 |
4422 | FAILED | 단말기 일시정지 |
4423 | FAILED | 단말기 착신거부 |
4424 | FAILED | URL SMS 미지원폰 |
4425 | FAILED | 단말기 호 처리 중 |
4426 | FAILED | 재시도 한도 초과 |
4427 | FAILED | 기타 단말기 문제 |
4428 | FAILED | 시스템 에러 |
4430 | FAILED | 스팸 |
4431 | FAILED | 발송 제한 수신거부(스팸) |
4432 | FAILED | 회신번호 차단(개인) |
4433 | FAILED | 회신번호 차단(기업) |
4434 | FAILED | 회신번호 사전 등록제에 의한 미등록 차단 |
4435 | FAILED | KISA 신고 스팸 회신 번호 차단 |
4436 | FAILED | 회신번호 사전 등록제 번호규칙 위반 |
4437 | FAILED | 중복 발신 제한 |
4438 | FAILED | 화이트리스트 문구 체크 |
4443 | FAILED | KISA 스팸 차단 |
LMS · MMS 결과 코드 (36)
| code | status | 설명 |
|---|---|---|
6600 | DELIVERED | 전달 |
6601 | FAILED | 타임 아웃 |
6602 | FAILED | 핸드폰 호 처리 중 |
6603 | FAILED | 음영 지역 |
6604 | FAILED | 전원이 꺼져 있음 |
6605 | FAILED | 메시지 저장 개수 초과 |
6606 | FAILED | 잘못된 번호 |
6607 | FAILED | 서비스 일시 정지 |
6608 | FAILED | 기타 단말기 문제 |
6609 | FAILED | 착신 거절 |
6610 | FAILED | 기타 에러 |
6611 | FAILED | 통신사의 SMC 형식 오류 |
6612 | FAILED | 게이트웨이의 형식 오류 |
6613 | FAILED | 서비스 불가 단말기 |
6614 | FAILED | 핸드폰 호 불가 상태 |
6615 | FAILED | SMC 운영자에 의해 삭제 |
6616 | FAILED | 통신사의 메시지 큐 초과 |
6617 | FAILED | 통신사의 스팸 처리 |
6618 | FAILED | 공정위의 스팸 처리 |
6619 | FAILED | 게이트웨이의 스팸 처리 |
6620 | FAILED | 발송 건수 초과 |
6621 | FAILED | 메시지의 길이 초과 |
6622 | FAILED | 잘못된 번호 형식 |
6623 | FAILED | 잘못된 데이터 형식 |
6624 | FAILED | MMS 정보를 찾을 수 없음 |
6625 | FAILED | NPDB 에러 |
6626 | FAILED | 080 수신거부(SPAM) |
6627 | FAILED | 발송 제한 수신거부(SPAM) |
6628 | FAILED | 회신 번호 차단(개인) |
6629 | FAILED | 회신 번호 차단(기업) |
6630 | FAILED | 서비스 불가 번호 |
6631 | FAILED | 회신 번호 사전 등록제에 의한 미등록 차단 |
6632 | FAILED | KISA 신고 스팸 회신 번호 차단 |
6633 | FAILED | 회신 번호 사전 등록제 번호 규칙 위반 |
6641 | FAILED | 스팸 차단 |
6670 | FAILED | 첨부파일 사이즈 초과(60K) |
알림톡 · 브랜드메시지 결과 코드 (106)
| code | status | 설명 |
|---|---|---|
7000 | DELIVERED | 전달 |
7058 | FAILED | 요청한 데이터가 없음 (발신프로필을 찾을 수 없음 등) |
7081 | FAILED | 요청을 처리하기 위한 권한 없음 (브랜드 메시지 API를 사용할 수 없는 허브파트너) |
7101 | FAILED | 파라미터 형식 오류 |
7103 | FAILED | Sender key (발신프로필키) 유효하지 않음 |
7105 | FAILED | Sender key (발신프로필키) 존재하지 않음 |
7106 | FAILED | 삭제된 Sender key (발신프로필키) |
7107 | FAILED | 차단 상태 Sender key (발신프로필키) |
7108 | FAILED | 차단 상태 카카오톡 채널 (카카오톡 채널 운영툴에서 확인) |
7109 | FAILED | 닫힌 상태 카카오톡 채널 (카카오톡 채널 운영툴에서 확인) |
7110 | FAILED | 삭제된 카카오톡 채널 (카카오톡 채널 운영툴에서 확인) |
7111 | FAILED | 삭제 대기 상태의 카카오톡 채널 (카카오톡 채널 운영툴에서 확인) |
7112 | FAILED | 유효하지 않은 사업자번호 |
7113 | FAILED | 유효하지 않은 app 연결 |
7115 | FAILED | 유효하지 않은 app user id 요청 |
7125 | FAILED | 메시지 차단 상태의 카카오톡 채널 (카카오톡 채널 운영툴에서 확인) |
7126 | FAILED | 해당 message type에서 사용할 수 없는 response_method로 요청 |
7127 | FAILED | 채널 메시지 제재 상태로 인한 메시지 전송 실패 |
7128 | FAILED | 해당 타겟팅 옵션 사용할 수 없음 |
7133 | FAILED | 템플릿 메시지 타입과 chat_bubble_type 파라미터 불일치 |
7203 | FAILED | 친구톡 전송 시 친구 대상 아님 |
7204 | FAILED | 템플릿 불일치 |
7206 | FAILED | 시리얼넘버 형식 불일치 |
7300 | FAILED | 기타 에러 |
7304 | FAILED | 지원하지 않는 스펙 |
7305 | FAILED | 성공 불확실 (30일 이내 수신 가능) |
7306 | FAILED | 카카오 시스템 오류 |
7307 | FAILED | 처리가 지연된 메시지 |
7308 | FAILED | 전화번호 오류 |
7311 | FAILED | 메시지가 존재하지 않음 |
7314 | FAILED | 메시지 길이 초과 |
7315 | FAILED | 템플릿 없음 |
7318 | FAILED | 메시지를 전송할 수 없음 |
7319 | FAILED | 톡 유저가 아님 |
7320 | FAILED | 알림톡 수신 차단 |
7321 | FAILED | 카카오톡 최소 버전 미지원 |
7322 | FAILED | 메시지 발송 불가 시간 |
7323 | FAILED | 메시지에 포함된 비디오를 전송할 수 없음 |
7324 | FAILED | 메시지에 포함된 이미지를 전송할 수 없음 |
7325 | FAILED | 변수 글자수 제한 초과 |
7326 | FAILED | 상담/봇 전환 버튼 글자수 제한 초과 |
7327 | FAILED | 버튼/바로 연결 내용과 템플릿 불일치 |
7328 | FAILED | 메시지 강조 표기 타이틀과 템플릿 불일치 |
7329 | FAILED | 메시지 강조 표기 타이틀 길이 제한 초과 (50자) |
7330 | FAILED | 메시지 타입과 템플릿 강조유형이 일치하지 않음 |
7331 | FAILED | 헤더가 템플릿과 일치하지 않음 |
7332 | FAILED | 헤더 길이 제한 초과 (16자) |
7333 | FAILED | 아이템 하이라이트가 템플릿과 일치하지 않음 |
7334 | FAILED | 아이템 하이라이트 타이틀 길이 제한 초과 (이미지 없는 경우 30자, 이미지 있는 경우 21자) |
7335 | FAILED | 아이템 하이라이트 디스크립션 길이 제한 초과 (이미지 없는 경우 19자, 이미지 있는 경우 13자) |
7336 | FAILED | 아이템 리스트가 템플릿과 일치하지 않음 |
7337 | FAILED | 아이템 리스트의 아이템 디스크립션 길이 제한 초과 (23자) |
7338 | FAILED | 아이템 요약정보가 템플릿과 일치하지 않음 |
7339 | FAILED | 아이템 요약정보 디스크립션 길이 제한 초과 (14자) |
7340 | FAILED | 아이템 요약정보 디스크립션에 허용되지 않은 문자 포함 (통화기호/코드·숫자·콤마·소수점·공백 외 불가) |
7341 | FAILED | 와이드 아이템 리스트 갯수 최소·최대 불일치 |
7342 | FAILED | 대표링크가 템플릿과 일치하지 않음 |
7343 | FAILED | 이미지 변수 개수 템플릿 불일치 |
7344 | FAILED | 커머스 쿠폰 변수 템플릿 불일치 |
7345 | FAILED | 커머스 정보 변수 템플릿 불일치 |
7346 | FAILED | 부가 정보 최대 길이 제한 오류 |
7347 | FAILED | 커머스 정보 상품명 최대 길이 제한 오류 |
7348 | FAILED | 유효하지 않은 그룹 태그 키 입력 |
7350 | FAILED | 수신동의거부 스펙 (N타입) 미지원 |
7351 | FAILED | 캐러셀 아이템 리스트 갯수 최소·최대 불일치 |
7352 | FAILED | 캐러셀 아이템 메시지 길이 초과 |
7353 | FAILED | 캐러셀이 템플릿과 일치하지 않음 |
7356 | FAILED | 와이드 리스트 타이틀 길이 제한 오류 |
7358 | FAILED | 캐러셀 헤더 길이 제한 오류 |
7400 | FAILED | 입력값 오류 발생 |
7401 | FAILED | Request Body Parsing Error |
7410 | FAILED | 시리얼 넘버 오류 |
7411 | FAILED | 시리얼 넘버 Prefix 날짜 오류 |
7412 | FAILED | Template 요청 오류 |
7413 | FAILED | 유효한 허브파트너를 찾을 수 없음 |
7414 | FAILED | 유효한 발신프로필을 찾을 수 없음 |
7415 | FAILED | 유효한 계약을 찾을 수 없음 |
7419 | FAILED | 유효하지 않은 챗버블 타입 또는 메시지 타입 요청 |
7420 | FAILED | 메시지 요청 페이로드 생성 오류 |
7421 | FAILED | 타임아웃 |
7422 | FAILED | 메시지 결과 조회 오류 |
7423 | FAILED | 메시지 발송 대상 오류 |
7432 | FAILED | 템플릿 변수 불일치 |
7433 | FAILED | 중단된 템플릿 |
7434 | FAILED | 변경된 템플릿 |
7435 | FAILED | 지갑 정보를 찾을 수 없음 |
7436 | FAILED | 지갑 잔액 부족 |
7437 | FAILED | 메시지 요청 실패 |
7438 | FAILED | 브랜드 메시지 개수 제한 |
7443 | FAILED | 만료된 요청 |
7444 | FAILED | 본문 길이 제한 (30KB) 초과 |
7450 | FAILED | 계약을 찾을 수 없음 |
7451 | FAILED | 중복된 계약 |
7452 | FAILED | 이미 종료된 계약 |
7453 | FAILED | 처리중인 계약 |
7454 | FAILED | 계약 생성 실패 |
7455 | FAILED | 계약 종료 실패 |
7456 | FAILED | 최대 발송수 초과 |
7460 | FAILED | 개인화 메시지 과금 정보를 찾을 수 없음 |
7461 | FAILED | 처리중인 메시지 |
7470 | FAILED | 동보 발송 요청 실패 |
7471 | FAILED | 동보 발송 요청을 찾을 수 없음 |
7472 | FAILED | 동보 발송 요청 상태를 변경할 수 없음 |
7521 | FAILED | 중복발신제한 |
7522 | FAILED | 스팸 차단 |
7523 | FAILED | 080 수신거부 (SPAM) |
서명 검증 (필수)
요청이 레드빈8 에서 온 것인지 확인하려면 X-Redbean-Signature 를 검증하세요.
signature = "v1=" + hex( HMAC_SHA256( secret, timestamp + "." + raw_body ) )
timestamp는X-Redbean-Timestamp헤더 값,raw_body는 파싱하기 전 원본 바이트입니다. (JSON 을 다시 직렬화하면 서명이 달라집니다)- 비교는 상수 시간 비교(timing-safe)로 하세요.
- 재전송 공격을 막기 위해
timestamp가 현재 시각과 5분 이상 차이나면 거부하세요.
Python (Flask)
import hmac, hashlib, time, json
from flask import Flask, request, abort
SECRET = "whsec_..." # 환경변수/비밀 저장소에서 읽으세요
app = Flask(__name__)
@app.post("/redbean/webhook")
def webhook():
raw = request.get_data() # 원본 바이트
ts = request.headers.get("X-Redbean-Timestamp", "")
sig = request.headers.get("X-Redbean-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(400)
expected = "v1=" + hmac.new(SECRET.encode(), f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
payload = json.loads(raw)
for ev in payload["events"]:
handle_event(ev) # event + recipient_id 로 멱등 처리
return "", 200Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.REDBEAN_WEBHOOK_SECRET;
const app = express();
// 서명 검증을 위해 원본 바이트를 그대로 받습니다.
app.post("/redbean/webhook", express.raw({ type: "application/json", limit: "2mb" }), (req, res) => {
const ts = req.get("X-Redbean-Timestamp") ?? "";
const sig = req.get("X-Redbean-Signature") ?? "";
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
const expected = "v1=" + crypto.createHmac("sha256", SECRET)
.update(Buffer.concat([Buffer.from(ts + "."), req.body])).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(sig);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
for (const ev of JSON.parse(req.body.toString("utf8")).events) handleEvent(ev);
res.sendStatus(200);
});PHP
<?php
$secret = getenv('REDBEAN_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // 원본 바이트
$ts = $_SERVER['HTTP_X_REDBEAN_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_REDBEAN_SIGNATURE'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300) { http_response_code(400); exit; }
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }
foreach (json_decode($raw, true)['events'] as $ev) {
handleEvent($ev); // event + recipient_id 로 멱등 처리
}
http_response_code(200);응답 규칙
- 2xx 응답이면 성공으로 처리합니다. 응답 본문은 무시합니다.
- 연결 3초, 응답 10초 이내에 응답해야 합니다. 무거운 처리는 큐에 넣고 즉시 200 을 반환하세요.
- 2xx 가 아니거나 시간 초과·연결 오류면 실패로 보고 재시도합니다.
재시도와 중복 수신
| 시도 | 다음 재시도까지 |
|---|---|
| 1회 실패 후 | 30초 |
| 2회 | 2분 |
| 3회 | 10분 |
| 4회 | 30분 |
| 5회 | 2시간 |
| 6회 | 6시간 |
| 7회 | 12시간 |
| 8회 | 24시간 |
| 9회 실패 | 전송 포기(DEAD) — 이후에도 Webhook 관리에서 수동 재전송 가능 |
- 같은 이벤트가 중복 전달될 수 있습니다(at-least-once).
event + recipient_id로 멱등 처리하세요. - 전달 순서는 보장되지 않습니다. 상태 비교가 필요하면 조회 API 로 최종 상태를 확인하세요.
- 수신 서버 장애로 연속 50회 실패하면 Webhook 이 일시 중지(PAUSED) 됩니다. 이벤트는 보존되며, 서버를 복구한 뒤 Webhook 관리에서 재개하면 밀린 이벤트가 전송됩니다.
누락 대비: Webhook 은 보조 채널이 아니라 주 채널로 쓰되, 하루 1회 정도 조회 API 로 pending 이 남은 건이나 최근 발송 결과를 대사(reconcile)하면 가장 안전합니다.
시크릿 교체
Webhook 관리에서 시크릿을 재발급하면 즉시 새 시크릿으로 서명됩니다. 수신 서버의 시크릿을 먼저 준비한 뒤 재발급하세요. 새 시크릿 원문은 재발급 직후 한 번만 표시됩니다.
보안 권장사항
- 서명 검증을 반드시 구현하세요. URL 만 알면 누구나 요청을 보낼 수 있습니다.
- 수신 엔드포인트에 별도 인증 정보를 URL 로 노출하지 마세요. (예:
?token=) 로그에 남을 수 있습니다. - 방화벽에서 IP 를 제한해야 한다면 고객센터에 발신 IP 대역을 문의하세요.