# Transcript Webhook (/docs/webhooks/transcript)

Transcript Webhook은 통화 녹음에 대한 STT(Speech-to-Text) 전사가 완료되거나 실패했을 때 설정된 URL로 알림을 전송합니다.
전사는 통화 종료 후 시작되며, **오디오 길이에 비례한 처리 시간**이 필요합니다. 계정 레벨에서 REST API로 등록합니다.

Transcript Webhook을 받으려면 먼저 **부가서비스 → 통화 받아쓰기** 를 활성화해야 합니다.
받아쓰기는 통화 녹음이 켜져 있을 때만 동작하며, 전사된 분량만큼 사용량 기반으로 과금되는 유료 기능입니다.
발신 통화의 녹음 시작 시점이 **신호음부터** 면 받지 않은 통화도 받아쓰기되고, 받기 전 구간을 포함한 녹음 길이로 과금됩니다([녹음 시작 시점](/docs/call-recording#녹음-시작-시점)).

### 등록

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

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

### 배송 로그 조회

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

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

## 이벤트 종류

- `transcript.completed` (event, 선택): 전사 완료 — TranscriptUrl 포함
- `transcript.failed` (event, 선택): 전사 실패 — Stage/ErrorMessage 포함

## 요청 파라미터

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

### transcript.completed

전사가 정상 종료되어 결과 JSON 이 GCS 에 업로드된 직후 전송됩니다.

- `CallId` (string, 필수): 통화 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 통화 방향 (inbound, outbound)
- `Event` (string, 필수): 고정값 transcript.completed
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)
- `TranscriptUrl` (string, 필수): 전사 결과 JSON GCS 서명 URL (7일 만료). 화자 분리와 타임스탬프가 포함된 segment 배열을 반환합니다.
- `DurationSec` (string, 필수): 오디오 길이(초) — 과금 분량과 동일. 정수 또는 소수점 한 자리
- `SegmentCount` (string, 필수): 전사된 세그먼트(발화 단위) 개수


```
CallId=CAabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=inbound&Event=transcript.completed&Timestamp=2026-04-23T09:12:44.123Z
&TranscriptUrl=https%3A%2F%2Fstorage.googleapis.com%2F...&DurationSec=87.4
&SegmentCount=23
```

### transcript.failed

전사 Job 이 중단되어 결과가 생성되지 않았을 때 전송됩니다.
`Stage` 로 실패 단계를, `ErrorMessage` 로 원인을 구분할 수 있습니다.

- `CallId` (string, 필수): 통화 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 통화 방향 (inbound, outbound)
- `Event` (string, 필수): 고정값 transcript.failed
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)
- `Stage` (string, 필수): 실패 단계. download(녹음 파일 다운로드 실패) 또는 runtime(ASR/업로드 중 예외)
- `ErrorMessage` (string, 필수): 사람 읽을 수 있는 오류 메시지


```
CallId=CAabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=inbound&Event=transcript.failed&Timestamp=2026-04-23T09:10:02.987Z
&Stage=download&ErrorMessage=rx%2Ftx+missing
```

## TranscriptUrl 응답 형식

`TranscriptUrl`을 GET하면 다음 형태의 JSON이 반환됩니다.

```json
[
  {
    "speaker": "speaker_0",
    "start": 0.42,
    "end": 2.11,
    "text": "안녕하세요, 예약 가능한가요?"
  },
  {
    "speaker": "speaker_1",
    "start": 2.35,
    "end": 4.80,
    "text": "네, 가능합니다. 언제로 도와드릴까요?"
  }
]
```

`speaker` 는 화자별로 부여되는 익명 식별자입니다. 2026년 8월 이후 전사는 `speaker_0`, `speaker_1`, … 형식이며, 전환(transfer) 통화처럼 참여자가 셋 이상이면 그만큼 늘어납니다. 그 이전 전사에는 `AGENT` / `CUSTOMER` 값이 그대로 남아 있으므로 두 형식을 모두 처리해야 합니다.

화자와 역할(고객 / 상담원 / AI)의 연결은 **보장하지 않습니다.** 라벨 번호와 실제 역할의 대응은 통화마다 달라질 수 있습니다.

## 예제

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

app = Flask(__name__)

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

    if event == "transcript.completed":
        url = request.form.get("TranscriptUrl")
        duration = request.form.get("DurationSec")
        count = request.form.get("SegmentCount")
        print(f"전사 완료 [{call_id}]: {count} segments, {duration}s")

        # 서명 URL 로 전사 본문 받기
        with urllib.request.urlopen(url) as r:
            segments = json.loads(r.read())
        for seg in segments:
            print(f"  [{seg['speaker']}] {seg['text']}")

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

    return "", 204
```


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

`TranscriptUrl`은 7일 후 만료됩니다. 장기 보관이 필요하면 webhook 수신 즉시 JSON을 다운로드해 자체 스토리지에 저장하세요.

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

