# Call Flow Webhook (/docs/webhooks/callflow)

Call Flow Webhook은 **콜 플로우(결정형 ARS)가 통화에서 수집한 값**을 통화가 끝난 직후 전달합니다.
발신자가 누른 번호, 녹음한 음성, HTTP 블럭이 조회한 값이 한 번에 담겨 옵니다. 계정 레벨에서 REST API로 등록합니다.

통화 **중** 실시간으로 값을 받고 싶다면 이 webhook 대신 플로우 안에 **HTTP 요청 블럭**을 두세요.
원하는 시점에 원하는 형태로 보낼 수 있습니다. `callflow.ended` 는 "통화가 끝났고 이만큼 모였다" 를 한 번 알리는 용도입니다.

[배치 발신(캠페인)](/docs/call-batches) 의 수신자별 결과도 이 이벤트로 받습니다 — 통화 1건이 끝날 때마다 즉시 도착하므로
대량 배치에서는 폴링보다 이쪽이 낫습니다.

### 등록

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

Body:
{
  "url": "https://my-app.com/callflow-webhook",
  "events": ["callflow.ended"]
}
```

## 이벤트 종류

- `callflow.ended` (event, 선택): 콜 플로우 통화 종료 — 수집 변수 전체 + Status(완주/이탈) 포함

**끝까지 진행한 통화와 중간에 끊긴 통화 모두** 이 이벤트를 보냅니다. 둘은 `Status` 로 구분하세요.
중도 이탈도 그때까지 모인 값이 그대로 담겨 옵니다 — 버리지 말고 부분 응답으로 활용할 수 있습니다.

## 요청 파라미터

모든 요청은 `Content-Type: application/x-www-form-urlencoded` POST 입니다.

- `CallId` (string, 필수): 통화 ID
- `AccountId` (string, 필수): 계정 ID
- `From` (string, 필수): 발신 번호
- `To` (string, 필수): 수신 번호
- `Direction` (string, 필수): 통화 방향 (inbound, outbound)
- `Event` (string, 필수): 고정값 callflow.ended
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)
- `FlowId` (string, 필수): 이 통화를 처리한 콜 플로우 ID
- `Status` (string, 필수): completed = 종료 블럭까지 정상 도달 / abandoned = 그 전에 통화가 끊김(부분 데이터)
- `Variables` (string, 필수): 수집 변수 전체를 담은 JSON 객체 문자열. 값이 없으면 {} 입니다 — JSON.parse 해서 사용하세요.
- `VariablesTruncated` (string, 선택): 고정값 true. 변수 전체가 8KB를 넘어 일부 키가 잘렸을 때만 포함됩니다.


```
CallId=CAabc123&AccountId=ACxxx&From=%2B821011112222&To=%2B821033334444
&Direction=outbound&Event=callflow.ended&Timestamp=2026-07-27T09:12:44.123Z
&FlowId=cmryw3ycm000001s6on0kp9a8&Status=completed
&Variables=%7B%22choice%22%3A%222%22%2C%22preferred_time%22%3A%22https%3A...%22%7D
```


`Status` 는 문자열입니다. **불리언처럼 쓰지 마세요** — form 인코딩에서는 모든 값이 문자열이라
`if (body.Status)` 같은 검사는 `abandoned` 에도 참이 됩니다. 반드시 값을 비교하세요.

```js
if (body.Status === "completed") { /* 완주 */ }
```


## Variables — 무엇이 담기나

플로우가 통화 중 만든 값이 전부 담깁니다.

- `메뉴 입력` (블럭 설정, 선택): 메뉴 블럭의 '입력 캡처' 에 변수명을 넣으면 누른 키가 담깁니다. 비워두면 분기만 하고 값은 남지 않습니다.
- `녹음` (블럭 설정, 선택): 녹음 블럭의 '녹음 저장' 에 변수명을 넣으면 그 이름으로 녹음 URL 이, <이름>_duration 에 길이(초)가 담깁니다.
- `외부 조회 결과` (블럭 설정, 선택): HTTP 요청 블럭의 saveAs 로 응답 JSON 에서 뽑은 값
- `직접 만든 값` (블럭 설정, 선택): 변수 설정(setvar) 블럭으로 만든 값
- `전환 결과` (블럭 설정, 선택): 전환 블럭의 '결과를 담을 변수' 에 변수명을 넣으면 그 전환의 결과(completed · no-answer · busy · failed)가 담깁니다.
- `시작 변수` (발신 시 주입, 선택): createCall 의 Variables 로 넣은 값(고객 이름 등)이 그대로 되돌아옵니다.
- `caller / callee` (자동, 필수): 발신·수신 번호. 항상 포함됩니다.
- `call.direction / call.customer_number / call.agent_number` (자동, 선택): 통화 방향(inbound · outbound), 통화 상대 번호, 이 통화에 쓰인 우리 번호. 착신이면 상대는 건 사람, 발신이면 받은 사람입니다. 착신 발신자가 번호 표시제한을 걸었으면 통화 상대 번호는 빈 문자열입니다. 에이전트 [통화 변수](/docs/voice-agent/call-variables)와 같은 이름입니다.
- `recording_url / recording_duration` (자동, 선택): 녹음 블럭이 있으면 **마지막** 녹음의 URL·길이. 녹음이 여러 개면 블럭마다 '녹음 저장' 이름을 지정해 따로 받으세요.
- `dial_status` (자동, 선택): 전환 블럭이 끝나면 **마지막** 전환의 결과: completed(받음) · no-answer(무응답) · busy(통화중) · failed(실패). 발신자가 전환 중에 끊으면 담기지 않습니다.

## 전환 결과별 연결

전환 블럭은 결과마다 다음 블럭을 따로 정할 수 있습니다. 비워 둔 결과는 '그 외' 로 갑니다.

- `받음` (completed, 선택): 상대가 받고 통화가 끝난 뒤
- `무응답` (no-answer, 선택): 벨 대기 시간 안에 받지 않음
- `통화중` (busy, 선택): 상대가 통화 중
- `실패` (failed, 선택): 거절, 번호 오류 등으로 연결되지 않음

녹음 블럭이 여러 개인데 '녹음 저장' 이름을 지정하지 않으면 **마지막 녹음이 앞의 것을 덮어씁니다**
(모두 `recording_url` 하나를 씁니다). 질문마다 답을 따로 받으려면 블럭별로 다른 이름을 주세요.

### 녹음 URL 다운로드

`Variables` 안의 녹음은 **90일간 보관**됩니다. 더 오래 두어야 하면 그 전에 내려받으세요.
조회에는 다른 API 와 같이 **API 키 인증이 필요**합니다 — `Authorization: Bearer <API 키>` 헤더를 붙여
GET 하면 녹음 파일(WAV)이 내려옵니다. 자세한 규격은 [녹음 조회 API](/docs/api/calls/getVerbRecording) 를 보세요.

```bash
curl -L -H "Authorization: Bearer $CLAWOPS_API_KEY" \
  "https://api.claw-ops.com/v1/accounts/{accountId}/recordings/{callId}/verb/{recordingName}" \
  -o answer.wav
```


2026년 7월 28일 이전에 받은 녹음 URL은 **24시간짜리 서명 URL**이라 이미 만료됐을 수 있습니다.
링크가 만료됐다고 파일까지 사라진 것은 아니니, 보관 기간(90일) 안의 녹음은 콘솔의 배치 결과
화면에서 그대로 재생·확인할 수 있습니다.

통화 **전체** 녹음(믹스 파일)은 이 webhook 이 아니라 [Recording Webhook](/docs/webhooks/recording) 으로 옵니다.
둘은 다른 파일입니다 — 여기 담기는 건 녹음 블럭이 받은 **그 질문의 답변**이고, Recording Webhook 은 통화 전체입니다.

## 예제

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

app = Flask(__name__)

@app.route("/callflow-webhook", methods=["POST"])
def callflow_webhook():
    # 서명 검증은 /docs/webhooks/signature-verification 참고 — 프로덕션에서는 필수
    call_id = request.form["CallId"]
    status = request.form["Status"]
    variables = json.loads(request.form.get("Variables", "{}"))

    if status == "completed":
        save_survey(call_id, request.form["From"], variables)
    else:
        # 중도 이탈 — 그때까지 모인 값만 들어 있습니다
        save_partial(call_id, request.form["From"], variables)

    if request.form.get("VariablesTruncated") == "true":
        log.warning("수집 변수가 8KB를 넘어 일부가 잘렸습니다: %s", call_id)

    return "", 200
```

```js
app.post("/callflow-webhook", express.urlencoded({ extended: false }), (req, res) => {
  const { CallId, From, Status, FlowId } = req.body
  const variables = JSON.parse(req.body.Variables || "{}")

  if (Status === "completed") {
    saveSurvey({ callId: CallId, phone: From, flowId: FlowId, ...variables })
  } else {
    savePartial({ callId: CallId, phone: From, ...variables })
  }

  res.sendStatus(200)   // 2xx 가 아니면 재시도됩니다
})
```


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

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

[배치 발신](/docs/call-batches)에서 재시도 차수(`Rounds` 2개 이상)를 쓰면 **한 수신자에게
통화가 여러 번 일어나므로 이 이벤트도 여러 번** 발생합니다. 각 통화의 `CallId` 는 서로 다르니
위의 중복 방지에는 걸리지 않습니다 — 수신자당 한 번을 가정한 처리가 있다면 전화번호 기준으로
따로 다뤄야 합니다.

## 발신 시 변수 넣기

플로우 멘트에 `{{이름}}` 이 있으면 발신할 때 값을 채워 보낼 수 있고, 그 값도 `Variables` 로 되돌아옵니다.

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

{
  "To": "01012345678",
  "From": "07052753864",
  "CallFlowId": "cmryw3ycm000001s6on0kp9a8",
  "Variables": { "name": "홍길동" }
}
```

어떤 변수를 넣어야 하는지는 콜 플로우 조회로 확인합니다.

```
GET /v1/accounts/{accountId}/call-flows

→ { "data": [{ "callFlowId": "cmryw...", "name": "고객센터 ARS", "variables": ["name"] }] }
```

`variables` 는 "플로우가 쓰지만 통화 중에는 만들어지지 않는 값" 입니다. 채우지 않으면 그 자리가 빈 문자열로 나갑니다.
