# Status Callback (/docs/webhooks/status)

Status Callback은 통화 상태가 변경될 때마다 지정한 URL로 이벤트를 전송합니다.

- **발신(outbound)**: 통화 생성 시 `status_callback` 파라미터로 통화마다 설정합니다.
- **수신(inbound)**: 외부에서 걸려오는 통화는 발신 시점이 없으므로, **전화번호 설정의 "수신 통화 상태 webhook"**(`status_callback`)에 URL을 미리 등록합니다. 대시보드 전화번호 상세 또는 번호 수정 API의 `status_callback` 으로 설정하며, 그 번호로 걸려오는 모든 통화의 상태 전이가 통지됩니다.

```python
call = client.calls.create(
    to="01012345678",
    from_="07052358010",
    url="https://my-app.com/twiml",
    status_callback="https://my-app.com/status",
    status_callback_event="initiated ringing answered completed",
)
```

## 이벤트 종류

`status_callback_event`에 수신할 이벤트를 공백으로 구분하여 지정합니다.

- `initiated` (event, 선택): 통화가 시작됨 (발신 시작)
- `ringing` (event, 선택): 상대방 전화벨이 울리는 중
- `answered` (event, 선택): 상대방이 전화를 받음
- `completed` (event, 선택): 통화가 종료됨
- `transfer` (event, 선택): 호전환 진행 상황 — 전환 시작·연결·종료마다 전송.

지정하지 않으면 `initiated ringing answered completed` 가 적용됩니다. `transfer` 는 여기에
포함되지 않으므로, 호전환 통지를 받으려면 **네 개와 함께 직접 나열**해야 합니다.

```
initiated ringing answered completed transfer
```

대시보드에서는 전화번호 상세 → 인바운드 라우팅 → 수정에서 체크박스로 켤 수 있습니다.

## 호전환 이벤트 (`transfer`)

호전환이 어떻게 됐는지 **통화가 끝나기 전에** 알 수 있습니다. 전환이 실패하면 다른 상담원으로
재시도하거나 안내 문자를 보내는 식으로 바로 이어갈 수 있습니다.

전환 한 건마다 시작(`initiated`) → 연결(`connected`) → 종료(`completed` / `failed` /
`no-answer` / `busy` / `canceled`) 순으로 전송됩니다. 연결되지 못하면 `connected` 없이
시작과 종료만 옵니다.

- `TransferStatus` (string, 필수): initiated(전환 시작) / connected(상대 응답, 통화 연결) / completed(정상 종료) / failed / no-answer / busy / canceled / interrupted(통화가 비정상 종료돼 시스템이 정리)
- `TransferSequence` (string, 필수): 이 통화의 몇 번째 전환 시도인지 (1부터). 다단 전환에서 어느 시도의 통지인지 구분합니다
- `TransferTo` (string, 선택): 전환 대상 번호
- `TransferMode` (string, 선택): 전환 방식
- `TransferDuration` (string, 선택): 전환 통화 시간(초) — 종료 이벤트에만 포함
- `TransferReason` (string, 선택): 에이전트가 파악한 문의 유형. 파악하지 못했으면 파라미터 자체가 오지 않습니다
- `TransferContext` (string, 선택): Voice Agent SDK 의 transfer({ context }) 로 직접 넘긴 구조화 데이터(JSON 문자열). 넘긴 그대로 전달되며, 넘기지 않았으면 파라미터 자체가 오지 않습니다
- `TransferStartedAt` (string, 선택): 이 전환 시도를 시작한 시각(ISO 8601, UTC)
- `TransferAnsweredAt` (string, 선택): 대상이 전화를 받은 시각(ISO 8601, UTC). 받지 않았으면 파라미터 자체가 오지 않습니다. TransferStatus 가 failed·canceled·no-answer 여도 이 값이 있으면 담당자는 받은 것입니다(연결 전 안내 도중 종료)
- `TransferConnectedAt` (string, 선택): 발신자와 대상이 연결된 시각(ISO 8601, UTC). 연결 전 안내(warm 전환)가 있으면 안내가 끝난 뒤 시각이라 TransferAnsweredAt 보다 늦습니다
- `TransferEndedAt` (string, 선택): 이 전환 시도가 끝난 시각(ISO 8601, UTC) — 종료 이벤트에만 포함
- `TransferTimeout` (string, 선택): 이 전환에 적용한 대기 한도(초). 대상이 이 시간 안에 받지 않으면 no-answer 로 끝납니다(SIP REFER 전환은 failed)
- `TransferSipResponseCode` (string, 선택): 통신망이 알려 준 SIP 응답코드 — 종료 이벤트에만, 값을 알 때만 포함
- `TransferHangupCauseQ850` (string, 선택): 통신망이 알려 준 Q.850 종료 사유 — 종료 이벤트에만, 값을 알 때만 포함
- `From` (string, 선택): 통화의 발신번호
- `To` (string, 선택): 통화의 수신번호
- `Direction` (string, 선택): inbound / outbound — From·To 중 어느 쪽이 고객인지 판단하는 근거입니다

이 이벤트는 아래 "요청 파라미터" 를 따르지 않습니다. `CallId` · `AccountId` · `Timestamp` ·
`From` · `To` · `Direction` 과 위 `Transfer*` 필드만 실립니다 — `CallStatus` 는 **없습니다**.
통화 자체의 상태 통지와 구분하려면 `TransferStatus` 필드의 존재 여부로 분기하세요.

`TransferReason` 과 `TransferContext` 는 **출처가 다릅니다**. 앞은 에이전트가 통화 중 파악한
내용이고, 뒤는 SDK 로 직접 넘긴 값이 그대로 전달된 것입니다. 둘 다 값이 없으면 파라미터 자체가
오지 않으므로, 읽기 전에 존재 여부를 확인하세요.

담당자가 받기까지 걸린 시간은 `TransferAnsweredAt - TransferStartedAt` 입니다.
`TransferConnectedAt` 은 연결 전 안내가 끝난 뒤 시각이라 이 계산에 쓰면 안내 길이만큼 길게 나옵니다.
`Timestamp` 는 웹훅을 보낸 시각이므로 전환 시각으로 쓰지 마세요.

같은 종료 이벤트가 **두 번 이상 도착할 수 있습니다**(전환 종료 처리가 여러 경로에서 겹칠 때).
`CallId` + `TransferSequence` + `TransferStatus` 조합을 키로 삼아 멱등하게 처리하세요.

## 요청 파라미터

기본 파라미터(`CallId`, `AccountId`, `Direction`)에 추가로 다음 파라미터가 포함됩니다.

- `CallStatus` (string, 필수): 현재 통화 상태. 진행: initiated/ringing/answered. 종료(completed 이벤트의 실제 사유): completed/busy/no-answer/failed/canceled/rejected. rejected/busy 는 통화가 연결되지 않고 거절된 경우이며, 이때 Duration 은 0 입니다. Twilio 는 거절한 통화를 no-answer 로 보내지만 한국AI통신은 rejected 로 보냅니다.
- `From` (string, 필수): 통화 발신번호
- `To` (string, 필수): 수신번호
- `ForwardedFrom` (string, 선택): 착신전환으로 들어온 수신 통화에서, 발신자가 원래 건 번호. 여러 번호를 한국AI통신 번호 하나로 착신전환했을 때 어느 번호로 걸려온 전화인지 구분합니다. 착신전환을 실행한 통신사가 전환 정보를 함께 보내온 경우에만 실리며, 직접 걸려온 통화와 발신 통화에는 파라미터 자체가 오지 않습니다
- `CallerIdRestricted` (string, 선택): 발신자가 번호 표시제한(`*23#` 등)을 걸고 건 통화면 `true`, 아니면 `false`. 번호는 `From` 에 그대로 전달되고, 표시제한 여부만 이 값으로 알려 드립니다. 통신사가 표시제한 여부를 보내지 않은 통화는 `false` 로 올 수 있습니다. 수신 통화의 상태 통지에만 실리며, 발신 통화와 전환 상태 통지(`TransferStatus`)에는 파라미터 자체가 오지 않습니다
- `Duration` (string, 선택): 통화 시간 (초) — completed 이벤트에서만 포함
- `Timestamp` (string, 필수): 이벤트 발생 시각 (ISO 8601)
- `Direction` (string, 필수): inbound(수신) 또는 outbound(발신)
- `AnsweredBy` (string, 선택): AMD(자동응답기 감지) 결과 — machine_detection 을 켠 발신 통화의 completed 이벤트에만 포함. human(사람) / machine(자동응답기·음성사서함) / unknown(판정 불가)
- `HangupCause` (string, 선택): 통화 종료 사유 — 종료 이벤트에만 포함. invalid_number(결번) / no_answer / user_busy / network_out_of_order 등. 전체 목록은 아래 참고
- `HangupCauseQ850` (string, 선택): 통신망 Q.850 cause code (1=결번, 16=정상해제, 17=통화중, 19=무응답, 38=망장애 등)
- `SipResponseCode` (string, 선택): 종료를 유발한 SIP 응답코드 (404=없는 번호, 486=통화중, 500=망 오류 등). 응답코드 없이 끝난 통화는 미포함
- `HangupSource` (string, 선택): 종료 책임 주체 — carrier(통신망) / callee(수신 측) / caller(발신 측) / app·system(통화 처리 — 웹훅 서버 응답 실패 포함). 정상 종료에서는 먼저 끊은 쪽의 역할입니다 — 착신 통화에서 상대가 끊으면 caller, 여러분의 앱이 끊으면 callee. 판정하지 못하면 미포함
- `ParentCallId` (string, 선택): 원래 통화의 ID — `<Dial>` 로 연결된 상대(수신 leg)의 통지에만 포함됩니다. 이때 CallId 는 그 연결만의 ID 이고, From/To 는 그 연결의 발신·수신번호입니다

`<Dial>` 로 연결한 **상대의 상태**도 따로 받을 수 있습니다 — `<Number>` / `<Sip>` 에 `statusCallback` 을 지정하면 그 목적지가 받은 시점(`answered`)과 끝난 시점(`completed`)이 통지됩니다. 이 통지에는 `ParentCallId` 가 함께 실려 어느 통화의 연결인지 구분됩니다. 자세한 내용은 [VoiceML `<Number>`](/docs/build/voiceml) 를 참고하세요.

`CallStatus: "failed"` 는 결번·망 오류·시스템 오류를 모두 포함하는 대분류입니다.
**결번인지 일시적 오류인지는 `HangupCause` 로 구분**하세요 —
[통화 실패 사유](/docs/call-failure-causes) 에 전체 목록과 재시도 판단 기준이 있습니다.

종료 사유 필드들은 통화가 끝난 이벤트(`completed` / `busy` / `no-answer` / `failed` /
`canceled` / `rejected`)에만 포함됩니다. 사유를 확정하지 못한 통화에서는 필드가 생략됩니다 —
값이 있을 때만 보내며, 없는 사유를 임의로 채우지 않습니다.

## 예제

```python
from flask import Flask, request

app = Flask(__name__)

@app.route("/status", methods=["POST"])
def status_callback():
    call_id = request.form.get("CallId")
    status = request.form.get("CallStatus")
    duration = request.form.get("Duration")

    print(f"통화 {call_id}: {status}")
    if status == "completed" and duration:
        print(f"  통화 시간: {duration}초")

    # 연결되지 않은 통화는 사유를 보고 재시도 여부를 정합니다.
    if status == "failed":
        cause = request.form.get("HangupCause")
        if cause == "invalid_number":
            print("  결번 — 목록에서 제외")
        elif request.form.get("HangupSource") in ("app", "system"):
            print("  번호 문제 아님 — 웹훅 서버·통화 처리 확인")

    return "", 204
```


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

