# Message Webhook (/docs/webhooks/message)

Message Webhook은 메시지 관련 이벤트가 발생하면 설정된 URL로 알림을 전송합니다.
문자(SMS/LMS/MMS)와 카카오 알림톡(`ata`)·브랜드 메시지(`bms`)는 **발송·실패에 같은 이벤트,
같은 파라미터**를 씁니다 — 무엇으로 나갔는지는 `Type` 이 알려줍니다. 수신은 문자만 있습니다.
계정 레벨에서 REST API로 등록합니다.

### 등록

```
POST /v1/accounts/{accountId}/webhooks

Body:
{
  "url": "https://my-app.com/message-webhook",
  "events": ["message.sent", "message.failed", "message.received"]
}
```

### 배송 로그 조회

SDK로 웹훅 배송 로그를 확인할 수 있습니다.

```python
page = client.webhook_logs.list("cmob6...", page=0, page_size=20)
for log in page.data:
    print(log)
```

## 이벤트 종류

- `message.sent` (event, 선택): 메시지 발송 완료
- `message.failed` (event, 선택): 메시지 발송 실패
- `message.received` (event, 선택): 메시지 수신 (인바운드)

## 요청 파라미터

모든 요청은 `Content-Type: application/x-www-form-urlencoded` POST 입니다.
이벤트마다 전달되는 파라미터가 다르니 아래 세 표를 참고하세요.

### message.sent

이동통신사로부터 최종 발송 성공 리포트를 수신한 직후 전송됩니다 (아웃바운드 전용).

- `MessageId` (string, 필수): 메시지 고유 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 고정값 outbound
- `Type` (string, 필수): 메시지 유형 (sms, lms, mms, ata, bms)
- `Status` (string, 필수): 고정값 sent
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)


```
MessageId=MGabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=outbound&Type=sms&Status=sent
&Timestamp=2026-04-23T09:12:44.123Z
```

### message.failed

발송 요청이 이동통신사 블랙리스트/데이터 오류 또는 실패 리포트로 종료된 경우 전송됩니다 (아웃바운드 전용).

- `MessageId` (string, 필수): 메시지 고유 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 고정값 outbound
- `Type` (string, 필수): 메시지 유형 (sms, lms, mms, ata, bms)
- `Status` (string, 필수): 고정값 failed
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)


```
MessageId=MGabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=outbound&Type=sms&Status=failed
&Timestamp=2026-04-23T09:12:44.123Z
```


**알림톡이 실패해 문자로 대체 발송되면 웹훅이 두 번 옵니다.** 먼저 `Type=ata` 로
`message.failed`, 이어서 `Type=sms`(또는 `lms`)로 `message.sent` 입니다. 대체 문자는
[별도의 메시지 1건](/docs/api/messages/sendMessage)이라 **`MessageId` 가 다릅니다** —
수신측에서 원래 메시지 하나로 합치려 하면 어긋납니다.

브랜드 메시지(`bms`)는 대체 발송이 없어 이 짝이 생기지 않습니다.

### message.received

인바운드 메시지가 도착해 저장(및 MMS 첨부 업로드)이 완료된 직후 전송됩니다.

- `MessageId` (string, 필수): 메시지 고유 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 고정값 inbound
- `Type` (string, 필수): 메시지 유형 (sms, lms, mms). 카카오는 수신이 없습니다.
- `Subject` (string, 필수): 메시지 제목. SMS 또는 제목이 없는 경우 빈 문자열
- `Body` (string, 필수): 메시지 본문
- `Status` (string, 필수): 고정값 received
- `NumMedia` (string, 필수): 첨부 미디어 수. MMS 가 아니면 0
- `MediaUrl0` (string, 선택): 첨부 미디어 조회 URL (MMS 에서 NumMedia 만큼 MediaUrl0, MediaUrl1 … 형태로 포함). API 키 인증이 필요한 엔드포인트이므로 Authorization 헤더를 붙여 요청하세요. 같은 첨부는 1시간에 10회까지 받을 수 있고, 넘으면 429 와 Retry-After 헤더가 돌아옵니다 — 받은 파일은 저장해 두고 다시 쓰세요.
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)


```
MessageId=MGabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=inbound&Type=mms&Subject=%EC%95%88%EB%82%B4&Body=%EC%98%A4%EB%8A%98+%EC%98%88%EC%95%BD
&Status=received&NumMedia=1&MediaUrl0=https%3A%2F%2Fapi.claw-ops.com%2Fv1%2Faccounts%2FACxxx%2Fmessages%2FMGabc123%2Fmedia%2F0
&Timestamp=2026-04-23T09:12:44.123Z
```

## 예제

```python
from flask import Flask, request

app = Flask(__name__)

@app.route("/message-webhook", methods=["POST"])
def message_webhook():
    status = request.form.get("Status")
    message_id = request.form.get("MessageId")

    if status == "received":
        body = request.form.get("Body", "")
        print(f"수신 메시지 [{message_id}]: {body}")
    elif status == "failed":
        to = request.form.get("To")
        print(f"발송 실패 [{message_id}] → {to}")
    elif status == "sent":
        print(f"발송 완료 [{message_id}]")

    return "", 204
```


Message Webhook 요청에도 `X-Signature` 헤더가 포함됩니다. [서명 검증 가이드](/docs/webhooks/signature-verification)를 참고하여 요청의 무결성을 확인하세요.

