# Summary Webhook (/docs/webhooks/summary)

Summary Webhook 은 통화 받아쓰기(transcript)가 완료된 뒤 LLM 이 생성한 **구조화된 요약(Structured Output)** 이 완료되거나 실패했을 때 설정된 URL 로 알림을 전송합니다.
요약 Job 은 transcript 가 완료된 직후 자동으로 실행되며, 계정 레벨에서 REST API 로 등록합니다.

Summary Webhook 을 받으려면 먼저 **부가서비스 → 통화 받아쓰기** 와 **부가서비스 → 통화 요약** 이 모두 활성화되어 있어야 합니다.
요약은 받아쓰기가 완료된 통화에 한해 실행되며, 사용량 기반으로 과금되는 유료 기능입니다.

### 등록

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

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

### 배송 로그 조회

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

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

## 이벤트 종류

- `summary.completed` (event, 선택): 요약 생성 완료 — SummaryJson 포함
- `summary.failed` (event, 선택): 요약 생성 실패 — Stage/ErrorMessage 포함

## 요청 파라미터

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

### summary.completed

LLM 호출이 정상 종료되어 결과 JSON 이 DB 에 저장된 직후 전송됩니다.

- `CallId` (string, 필수): 통화 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 통화 방향 (inbound, outbound)
- `Event` (string, 필수): 고정값 summary.completed
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)
- `Provider` (string, 필수): 요약에 사용된 LLM Provider (예: anthropic)
- `Model` (string, 필수): 요약에 사용된 모델 ID (예: claude-sonnet-5-5)
- `PromptVersion` (string, 필수): 사용된 시스템 프롬프트 버전. 기본 default:v1, 조직 커스텀 프롬프트는 org:{accountId}:{hash}
- `SchemaVersion` (string, 필수): 사용된 출력 스키마 버전. 기본 default:v1, 조직 커스텀 스키마는 org:{accountId}:{hash}
- `SummaryJson` (string, 필수): 요약 결과 JSON 을 string 으로 직렬화한 값. 수신 측에서 JSON.parse 로 파싱해서 사용


```
CallId=CAabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=inbound&Event=summary.completed&Timestamp=2026-04-23T09:13:08.456Z
&Provider=anthropic&Model=claude-sonnet-5-5
&PromptVersion=default%3Av1&SchemaVersion=default%3Av1
&SummaryJson=%7B%22coreSummary%22%3A%22...%22%2C%22decisions%22%3A%5B%5D%2C%22followUps%22%3A%5B%5D%2C%22sentiment%22%3A%22neutral%22%7D
```

### summary.failed

요약 Job 이 중단되어 결과가 생성되지 않았을 때 전송됩니다.

- `CallId` (string, 필수): 통화 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 통화 방향 (inbound, outbound)
- `Event` (string, 필수): 고정값 summary.failed
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)
- `Stage` (string, 필수): 실패 단계. runtime(LLM 호출/스키마 검증 중 예외)
- `ErrorMessage` (string, 필수): 사람 읽을 수 있는 오류 메시지


```
CallId=CAabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=inbound&Event=summary.failed&Timestamp=2026-04-23T09:13:10.012Z
&Stage=runtime&ErrorMessage=schema+validation+failed
```

## SummaryJson 형식

`SummaryJson` 은 조직이 등록한 출력 스키마(또는 기본 스키마)에 맞춰진 JSON 문자열입니다.
기본 스키마(`default:v1`) 를 사용하는 경우 다음 필드가 보장됩니다.

```json
{
  "coreSummary": "고객이 환불 절차를 문의함. 7일 이내 처리 안내됨.",
  "decisions": ["환불 신청서 이메일 발송"],
  "followUps": ["배송 운송장 번호 확인 후 회신"],
  "sentiment": "neutral"
}
```

- `coreSummary` — 핵심 요지 1~2 문장
- `decisions` — 주요 합의/결정사항 배열 (없으면 빈 배열)
- `followUps` — 후속 조치 배열 (없으면 빈 배열)
- `sentiment` — `positive` / `neutral` / `negative` 중 하나

조직에서 커스텀 프롬프트나 스키마를 등록한 경우 `PromptVersion` / `SchemaVersion` 이 `org:{accountId}:{hash}` 형태로 전달되며, `SummaryJson` 의 필드도 등록한 스키마에 따라 달라집니다.

## 예제

```python
from flask import Flask, request
import json

app = Flask(__name__)

@app.route("/summary-webhook", methods=["POST"])
def summary_webhook():
    event = request.form.get("Event")
    call_id = request.form.get("CallId")

    if event == "summary.completed":
        provider = request.form.get("Provider")
        model = request.form.get("Model")
        summary = json.loads(request.form.get("SummaryJson") or "{}")
        print(f"요약 완료 [{call_id}] ({provider}/{model})")
        print(f"  요지: {summary.get('coreSummary')}")
        for d in summary.get("decisions", []):
            print(f"  - 결정: {d}")
        for f in summary.get("followUps", []):
            print(f"  - 후속: {f}")

    elif event == "summary.failed":
        stage = request.form.get("Stage")
        err = request.form.get("ErrorMessage")
        print(f"요약 실패 [{call_id}] ({stage}): {err}")

    return "", 204
```


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

Summary Webhook 은 전달 보장(at-least-once) 방식이므로 드물게 **같은 이벤트가 두 번 이상 도착**할 수 있습니다.
수신 서버에서 `CallId`를 idempotency key 로 사용해 중복 처리를 방지하세요.

