# Recording Webhook (/docs/webhooks/recording)

Recording Webhook은 통화 녹음 파일이 GCS에 업로드되거나 업로드에 실패했을 때 설정된 URL로 알림을 전송합니다.
녹음은 통화 종료 직후 처리되며, 보통 수초 내에 webhook이 도착합니다. 계정 레벨에서 REST API로 등록합니다.

Recording Webhook을 받으려면 **부가서비스 → 통화 녹음** 이 활성화되어 있어야 합니다.
녹음이 시작되지 않은 통화 (예: 응답 없음, 회선 오류, 녹음 비활성 상태에서 발생한 통화) 는 `recording.completed`/`recording.failed` 둘 다 발생하지 않습니다.
단, 발신 통화의 [녹음 시작 시점](/docs/call-recording#녹음-시작-시점)이 **신호음부터** 면 상대가 받지 않은 통화도 녹음되어 `recording.completed` 가 전송됩니다.

### 등록

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

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

### 배송 로그 조회

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

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

## 이벤트 종류

- `recording.completed` (event, 선택): 녹음 mixed 파일 업로드 완료 — RecordingUrl 포함
- `recording.failed` (event, 선택): 녹음 mixed 파일 업로드 실패 — Stage/ErrorMessage 포함

## 요청 파라미터

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

### recording.completed

mixed 녹음 파일이 GCS에 업로드되고 DB에 `recording_url` 이 반영된 직후 전송됩니다.

- `CallId` (string, 필수): 통화 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 통화 방향 (inbound, outbound)
- `Event` (string, 필수): 고정값 recording.completed
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)
- `RecordingUrl` (string, 필수): 녹음 파일 다운로드 API 엔드포인트 절대 URL. API key 인증으로 GET 하면 15분 TTL 의 GCS 서명 URL 로 302 redirect 됩니다 — HTTP 클라이언트가 자동 follow.
- `DurationSec` (string, 선택): 통화 길이(초, 상대가 받은 뒤부터). 받지 않은 통화는 0. 일부 케이스에서 미포함될 수 있음
- `RecordingDurationSec` (string, 선택): 녹음 파일 길이(초, 올림). 녹음 시작 시점이 신호음부터인 발신 통화에만 포함
- `AnswerOffsetSec` (string, 선택): 녹음 시작 후 상대가 받기까지 걸린 시간(초). 녹음 시작 시점이 신호음부터인 발신 통화 중 받은 통화에만 포함


```
CallId=CAabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=inbound&Event=recording.completed&Timestamp=2026-04-23T09:12:44.123Z
&RecordingUrl=https%3A%2F%2Fapi.claw-ops.com%2Fv1%2Faccounts%2FACxxx%2Frecordings%2FCAabc123
&DurationSec=87
```

### recording.failed

녹음은 시작되었으나 (mixed 파일이 생성됨) GCS 업로드 단계에서 최종 실패했을 때 전송됩니다.

- `CallId` (string, 필수): 통화 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 통화 방향 (inbound, outbound)
- `Event` (string, 필수): 고정값 recording.failed
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)
- `Stage` (string, 필수): 실패 단계 — 현재 upload (GCS 업로드 재시도 후 최종 실패)
- `ErrorMessage` (string, 필수): 사람 읽을 수 있는 오류 메시지


```
CallId=CAabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=inbound&Event=recording.failed&Timestamp=2026-04-23T09:10:02.987Z
&Stage=upload&ErrorMessage=GCS+%EC%97%85%EB%A1%9C%EB%93%9C+%EC%B5%9C%EC%A2%85+%EC%8B%A4%ED%8C%A8+%28mixed%29
```

## RecordingUrl 다운로드 동작

`RecordingUrl` 은 한국AI통신 API 엔드포인트 절대 URL 입니다 (서명 URL 이 아님).
API key Bearer 인증으로 GET 하면 15분 TTL 의 GCS 서명 URL 로 **302 redirect** 됩니다.

```
GET https://api.claw-ops.com/v1/accounts/ACxxx/recordings/CAabc123
    Authorization: Bearer sk_...

→ 302 Location: https://storage.googleapis.com/...&X-Goog-Signature=...
→ 200 audio/wav (RIFF WAVE, 16-bit PCM mono 8kHz)
```

대부분의 HTTP 클라이언트 (`requests`, `httpx`, `axios`, `curl -L`, browser) 는 redirect 를 자동 follow 하므로 별도 처리가 필요 없습니다.

`RecordingUrl` 자체에는 자격증명이 들어가지 않습니다 — webhook payload 가 로그/메일에 새어도 API key 없이는 다운로드 불가합니다. 따라서 별도 만료 기간 없이 영구적으로 유효합니다 (단, 다운로드 시점마다 새 서명 URL 이 발급되며 그 서명만 15분 TTL).

## 예제

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

app = Flask(__name__)
CLAWOPS_API_KEY = "sk_..."

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

    if event == "recording.completed":
        url = request.form.get("RecordingUrl")
        duration = request.form.get("DurationSec")
        print(f"녹음 완료 [{call_id}]: {duration}s")

        # API key 로 GET → 자동 302 follow → wav 바이트 수신
        r = requests.get(url, headers={"Authorization": f"Bearer {CLAWOPS_API_KEY}"})
        r.raise_for_status()
        with open(f"{call_id}.wav", "wb") as f:
            f.write(r.content)

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

    return "", 204
```


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

녹음이 **시작되지 않은** 통화 (응답 없음, 회선 오류 등) 에는 `recording.failed` 가 발생하지 않습니다.
"녹음 실패" 와 "녹음 안 됨" 을 구분해서 처리하세요 — 후자는 `call.completed` 의 status 와 hangup cause 로 판별합니다.

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

