결과 Webhook

발송 결과가 확정되면 레드빈8 서버가 고객사가 등록한 URL 로 직접 HTTPS POST 합니다. 폴링 없이 결과를 실시간으로 반영할 수 있습니다.

설정

  1. 결과 Webhook 메뉴에서 수신 URL 을 등록합니다.
  2. 발급된 서명 시크릿(whsec_…)을 안전한 곳에 저장합니다. (원문은 발급 직후 한 번만 표시)
  3. 테스트 전송 버튼으로 연결을 확인합니다. (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-Typeapplication/json
X-Redbean-Eventmessage.batch (테스트는 webhook.test)
X-Redbean-Delivery-Id전송 시도 단위 ID (본문 id 와 동일). 재시도하면 새 값이 부여되므로 중복 처리 방지에는 event + recipient_id 를 사용하세요. 로그 추적용으로 활용합니다.
X-Redbean-Timestamp전송 시각 (Unix epoch 초). 재시도마다 갱신됩니다.
X-Redbean-Signaturev1= + HMAC-SHA256 16진수 (아래 참고)
User-AgentRedbean-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 도 같은 값을 사용합니다.

statuseventresult.code요금
DELIVEREDmessage.delivered성공 코드 (SMS 4100, LMS/MMS 6600 등 — 아래 표의 DELIVERED)차감 유지
FAILEDmessage.failed실패 코드 (아래 표의 FAILED). 표에 없는 코드는 message 가 “발송에 실패했습니다.”자동 환불
REJECTEDmessage.rejectedREJECTED (고정). 발송 대행 시스템 내부 사유는 공개하지 않습니다.자동 환불
CANCELLEDmessage.cancellednull자동 환불

채널별 상세 결과 코드는 아래와 같습니다. 코드는 통신사·카카오 정책에 따라 추가될 수 있으므로, 모르는 코드는 status 로 성공/실패를 판단하고 message 를 그대로 표시하세요.

SMS 결과 코드 (32)
codestatus설명
4100DELIVERED전달
4400FAILED음영 지역
4401FAILED단말기 전원 꺼짐
4402FAILED단말기 메시지 저장 초과
4403FAILED메시지 삭제 됨
4404FAILED가입자 위치 정보 없음
4405FAILED단말기 BUSY
4410FAILED잘못된 번호
4411FAILEDNPDB 에러
4412FAILED착신거절
4413FAILEDSMSC 형식오류
4414FAILED비가입자, 결번, 서비스정지
4415FAILED식별코드 오류
4420FAILED기타에러
4421FAILED타임아웃
4422FAILED단말기 일시정지
4423FAILED단말기 착신거부
4424FAILEDURL SMS 미지원폰
4425FAILED단말기 호 처리 중
4426FAILED재시도 한도 초과
4427FAILED기타 단말기 문제
4428FAILED시스템 에러
4430FAILED스팸
4431FAILED발송 제한 수신거부(스팸)
4432FAILED회신번호 차단(개인)
4433FAILED회신번호 차단(기업)
4434FAILED회신번호 사전 등록제에 의한 미등록 차단
4435FAILEDKISA 신고 스팸 회신 번호 차단
4436FAILED회신번호 사전 등록제 번호규칙 위반
4437FAILED중복 발신 제한
4438FAILED화이트리스트 문구 체크
4443FAILEDKISA 스팸 차단
LMS · MMS 결과 코드 (36)
codestatus설명
6600DELIVERED전달
6601FAILED타임 아웃
6602FAILED핸드폰 호 처리 중
6603FAILED음영 지역
6604FAILED전원이 꺼져 있음
6605FAILED메시지 저장 개수 초과
6606FAILED잘못된 번호
6607FAILED서비스 일시 정지
6608FAILED기타 단말기 문제
6609FAILED착신 거절
6610FAILED기타 에러
6611FAILED통신사의 SMC 형식 오류
6612FAILED게이트웨이의 형식 오류
6613FAILED서비스 불가 단말기
6614FAILED핸드폰 호 불가 상태
6615FAILEDSMC 운영자에 의해 삭제
6616FAILED통신사의 메시지 큐 초과
6617FAILED통신사의 스팸 처리
6618FAILED공정위의 스팸 처리
6619FAILED게이트웨이의 스팸 처리
6620FAILED발송 건수 초과
6621FAILED메시지의 길이 초과
6622FAILED잘못된 번호 형식
6623FAILED잘못된 데이터 형식
6624FAILEDMMS 정보를 찾을 수 없음
6625FAILEDNPDB 에러
6626FAILED080 수신거부(SPAM)
6627FAILED발송 제한 수신거부(SPAM)
6628FAILED회신 번호 차단(개인)
6629FAILED회신 번호 차단(기업)
6630FAILED서비스 불가 번호
6631FAILED회신 번호 사전 등록제에 의한 미등록 차단
6632FAILEDKISA 신고 스팸 회신 번호 차단
6633FAILED회신 번호 사전 등록제 번호 규칙 위반
6641FAILED스팸 차단
6670FAILED첨부파일 사이즈 초과(60K)
알림톡 · 브랜드메시지 결과 코드 (106)
codestatus설명
7000DELIVERED전달
7058FAILED요청한 데이터가 없음 (발신프로필을 찾을 수 없음 등)
7081FAILED요청을 처리하기 위한 권한 없음 (브랜드 메시지 API를 사용할 수 없는 허브파트너)
7101FAILED파라미터 형식 오류
7103FAILEDSender key (발신프로필키) 유효하지 않음
7105FAILEDSender key (발신프로필키) 존재하지 않음
7106FAILED삭제된 Sender key (발신프로필키)
7107FAILED차단 상태 Sender key (발신프로필키)
7108FAILED차단 상태 카카오톡 채널 (카카오톡 채널 운영툴에서 확인)
7109FAILED닫힌 상태 카카오톡 채널 (카카오톡 채널 운영툴에서 확인)
7110FAILED삭제된 카카오톡 채널 (카카오톡 채널 운영툴에서 확인)
7111FAILED삭제 대기 상태의 카카오톡 채널 (카카오톡 채널 운영툴에서 확인)
7112FAILED유효하지 않은 사업자번호
7113FAILED유효하지 않은 app 연결
7115FAILED유효하지 않은 app user id 요청
7125FAILED메시지 차단 상태의 카카오톡 채널 (카카오톡 채널 운영툴에서 확인)
7126FAILED해당 message type에서 사용할 수 없는 response_method로 요청
7127FAILED채널 메시지 제재 상태로 인한 메시지 전송 실패
7128FAILED해당 타겟팅 옵션 사용할 수 없음
7133FAILED템플릿 메시지 타입과 chat_bubble_type 파라미터 불일치
7203FAILED친구톡 전송 시 친구 대상 아님
7204FAILED템플릿 불일치
7206FAILED시리얼넘버 형식 불일치
7300FAILED기타 에러
7304FAILED지원하지 않는 스펙
7305FAILED성공 불확실 (30일 이내 수신 가능)
7306FAILED카카오 시스템 오류
7307FAILED처리가 지연된 메시지
7308FAILED전화번호 오류
7311FAILED메시지가 존재하지 않음
7314FAILED메시지 길이 초과
7315FAILED템플릿 없음
7318FAILED메시지를 전송할 수 없음
7319FAILED톡 유저가 아님
7320FAILED알림톡 수신 차단
7321FAILED카카오톡 최소 버전 미지원
7322FAILED메시지 발송 불가 시간
7323FAILED메시지에 포함된 비디오를 전송할 수 없음
7324FAILED메시지에 포함된 이미지를 전송할 수 없음
7325FAILED변수 글자수 제한 초과
7326FAILED상담/봇 전환 버튼 글자수 제한 초과
7327FAILED버튼/바로 연결 내용과 템플릿 불일치
7328FAILED메시지 강조 표기 타이틀과 템플릿 불일치
7329FAILED메시지 강조 표기 타이틀 길이 제한 초과 (50자)
7330FAILED메시지 타입과 템플릿 강조유형이 일치하지 않음
7331FAILED헤더가 템플릿과 일치하지 않음
7332FAILED헤더 길이 제한 초과 (16자)
7333FAILED아이템 하이라이트가 템플릿과 일치하지 않음
7334FAILED아이템 하이라이트 타이틀 길이 제한 초과 (이미지 없는 경우 30자, 이미지 있는 경우 21자)
7335FAILED아이템 하이라이트 디스크립션 길이 제한 초과 (이미지 없는 경우 19자, 이미지 있는 경우 13자)
7336FAILED아이템 리스트가 템플릿과 일치하지 않음
7337FAILED아이템 리스트의 아이템 디스크립션 길이 제한 초과 (23자)
7338FAILED아이템 요약정보가 템플릿과 일치하지 않음
7339FAILED아이템 요약정보 디스크립션 길이 제한 초과 (14자)
7340FAILED아이템 요약정보 디스크립션에 허용되지 않은 문자 포함 (통화기호/코드·숫자·콤마·소수점·공백 외 불가)
7341FAILED와이드 아이템 리스트 갯수 최소·최대 불일치
7342FAILED대표링크가 템플릿과 일치하지 않음
7343FAILED이미지 변수 개수 템플릿 불일치
7344FAILED커머스 쿠폰 변수 템플릿 불일치
7345FAILED커머스 정보 변수 템플릿 불일치
7346FAILED부가 정보 최대 길이 제한 오류
7347FAILED커머스 정보 상품명 최대 길이 제한 오류
7348FAILED유효하지 않은 그룹 태그 키 입력
7350FAILED수신동의거부 스펙 (N타입) 미지원
7351FAILED캐러셀 아이템 리스트 갯수 최소·최대 불일치
7352FAILED캐러셀 아이템 메시지 길이 초과
7353FAILED캐러셀이 템플릿과 일치하지 않음
7356FAILED와이드 리스트 타이틀 길이 제한 오류
7358FAILED캐러셀 헤더 길이 제한 오류
7400FAILED입력값 오류 발생
7401FAILEDRequest Body Parsing Error
7410FAILED시리얼 넘버 오류
7411FAILED시리얼 넘버 Prefix 날짜 오류
7412FAILEDTemplate 요청 오류
7413FAILED유효한 허브파트너를 찾을 수 없음
7414FAILED유효한 발신프로필을 찾을 수 없음
7415FAILED유효한 계약을 찾을 수 없음
7419FAILED유효하지 않은 챗버블 타입 또는 메시지 타입 요청
7420FAILED메시지 요청 페이로드 생성 오류
7421FAILED타임아웃
7422FAILED메시지 결과 조회 오류
7423FAILED메시지 발송 대상 오류
7432FAILED템플릿 변수 불일치
7433FAILED중단된 템플릿
7434FAILED변경된 템플릿
7435FAILED지갑 정보를 찾을 수 없음
7436FAILED지갑 잔액 부족
7437FAILED메시지 요청 실패
7438FAILED브랜드 메시지 개수 제한
7443FAILED만료된 요청
7444FAILED본문 길이 제한 (30KB) 초과
7450FAILED계약을 찾을 수 없음
7451FAILED중복된 계약
7452FAILED이미 종료된 계약
7453FAILED처리중인 계약
7454FAILED계약 생성 실패
7455FAILED계약 종료 실패
7456FAILED최대 발송수 초과
7460FAILED개인화 메시지 과금 정보를 찾을 수 없음
7461FAILED처리중인 메시지
7470FAILED동보 발송 요청 실패
7471FAILED동보 발송 요청을 찾을 수 없음
7472FAILED동보 발송 요청 상태를 변경할 수 없음
7521FAILED중복발신제한
7522FAILED스팸 차단
7523FAILED080 수신거부 (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 "", 200
Node.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 대역을 문의하세요.