# 통화 실패 사유 (Hangup Cause) (/docs/call-failure-causes)

## 통화 실패 사유 (Hangup Cause)

`status` 는 통화가 **어떻게** 끝났는지(`completed` / `failed` / `busy` / `no-answer` ...)만
알려줍니다. **왜** 그렇게 끝났는지 — 결번인지, 상대가 안 받은 건지, 망 장애인지 — 는
`hangupCause` 로 구분합니다.

특히 **`failed` 는 결번·망 오류·시스템 오류를 모두 포함하는 대분류**입니다.
발신 리스트를 정제하거나 재시도 여부를 판단하려면 `status` 가 아니라 `hangupCause` 를 보세요.

## 어디서 받나요

| 경로 | 필드 |
| --- | --- |
| [통화 단건 조회](/docs/api/calls/getCall) | `hangupCause` · `hangupCauseQ850` · `sipResponseCode` · `hangupSource` |
| [통화 목록 조회](/docs/api/calls/listCalls) | 위와 동일 (통화마다) |
| [Status Callback webhook](/docs/webhooks/status) | `HangupCause` · `HangupCauseQ850` · `SipResponseCode` · `HangupSource` (종료 이벤트) |

```json
{
  "callId": "CAabcdef1234567890",
  "status": "failed",
  "to": "07080588491",
  "duration": 0,
  "hangupCause": "invalid_number",
  "hangupCauseQ850": 1,
  "sipResponseCode": 404,
  "hangupSource": "carrier"
}
```


네 값은 **같은 사건을 서로 다른 해상도로** 표현합니다. 보통은 `hangupCause` 하나면 충분하고,
`hangupCauseQ850`·`sipResponseCode` 는 통신망 원본 신호가 필요한 정밀 분석용입니다.

## 재시도 판단

가장 흔한 용도입니다. 이 표만 보고 분기하면 됩니다.

### 재시도해도 소용없음 — 번호를 목록에서 제외하세요

| `hangupCause` | Q.850 | 뜻 |
| --- | --- | --- |
| `invalid_number` | 1 · 5 · 28 | **결번**. 없는 번호이거나 형식이 잘못됐습니다 |
| `number_changed` | 22 | 번호가 변경됐습니다 |
| `incompatible_destination` | 88 | 해당 번호로는 음성 통화를 연결할 수 없습니다 |
| `recipient_blocked` | 21 | **수신거부 명단**에 등록된 번호라 걸지 않았습니다. 벨은 울리지 않았고 요금도 발생하지 않습니다 |

`recipient_blocked` 는 통신망이 아니라 **여러분이 등록한 명단** 때문에 걸리지 않은 통화입니다.
재시도하면 같은 결과이고, 무엇보다 연락하지 말라고 한 상대에게 다시 거는 일이 됩니다.
명단은 [수신거부 목록 조회](/docs/api/blockedrecipients/listBlockedRecipients) 또는 콘솔의 수신거부 화면에서 관리하고,
해제해야 다시 발신됩니다. 발신 API 호출 자체는 `422 recipient_blocked` 로 즉시 거절됩니다.

### 재시도 가치 있음 — 일시적인 사유입니다

| `hangupCause` | Q.850 | 뜻 |
| --- | --- | --- |
| `no_answer` | 18 · 19 · 20 | 벨은 울렸으나 받지 않음 |
| `user_busy` | 17 | 통화 중 |
| `recovery_on_timer_expire` | 102 | 응답 대기 시간 초과 |
| `temporary_failure` | 41 | 통신망 일시 장애 |
| `switching_congestion` | 42 | 교환기 혼잡 |
| `no_circuit_available` | 34 | 회선 부족 |
| `network_out_of_order` | 38 | 망 장애 |
| `destination_out_of_order` | 27 | 상대 단말/교환기 장애 |
| `resource_unavailable` | 47 | 망 자원 부족 |
| `protocol_error` | 111 | 통신망 프로토콜 오류 |

### 계정 한도·상태 — 번호가 아니라 계정을 확인하세요

수신 통화가 계정 설정 때문에 연결되지 못한 경우입니다. 상대 번호에는 문제가 없습니다.

| `hangupCause` | Q.850 | 뜻 |
| --- | --- | --- |
| `concurrency_limit_exceeded` | 17 | **동시통화 한도를 넘어** 받지 못했습니다. 잠시 후 재시도하거나 동시통화 채널을 올리세요 |
| `subscription_inactive` | 21 | 구독이 없거나 만료되어 받지 못했습니다. 재시도해도 동일합니다 |
| `inbound_quota_exceeded` | 21 | 월 수신 통화 한도를 다 썼습니다. 다음 청구 주기에 초기화됩니다 |
| `inbound_not_supported` | 21 | 현재 플랜이 수신 통화를 지원하지 않습니다 |
| `sip_trunk_inactive` | 21 | SIP 트렁크 연결 부가서비스가 없거나 만료되어 BYOC·소프트폰 번호로 받지 못했습니다 |

1채널로 운영 중에 두 번째 전화가 들어오면 `concurrency_limit_exceeded` 로 통화 기록이 남고
[Status Callback webhook](/docs/webhooks/status) 이 발송됩니다. 발신번호(`From`)를 받아
콜백 안내 문자를 보내는 구성에 쓰실 수 있습니다.

### 수신 통화 차단 — 여러분의 설정이 거절했습니다

수신 통화를 **받지 않고 거절**하도록 여러분이 설정한 경우입니다. 실패가 아니며 요금도 청구되지 않습니다.
`status` 는 `rejected` 입니다. 설정 방법은 [수신 통화 차단](/docs/call-blocking) 을 보세요.

| `hangupCause` | Q.850 | 뜻 |
| --- | --- | --- |
| `caller_blocked` | 21 | **수신 통화 차단 목록**에 있는 번호라 받지 않고 거절했습니다 |
| `caller_id_restricted_rejected` | 21 | 번호의 **번호 표시제한 통화 거절**이 켜져 있어, 번호를 숨기고 건 통화를 받지 않고 거절했습니다 |

`recipient_blocked`(발신 쪽 수신거부 명단)와 다릅니다 — 그쪽은 여러분이 **거는** 통화를 막습니다.

### 등록 단말·SIP 도착지 — 받는 쪽 설정을 확인하세요

번호 라우팅을 **소프트폰(등록 단말 착신)** 이나 **SIP 트렁크(외부 SIP 도착지)** 로 둔 번호에 걸려온 전화를
넘기지 못한 경우입니다. 등록 단말은 `softphone_not_registered`, SIP 도착지는 `sip_endpoint_not_found` 로
경로가 갈립니다.

| `hangupCause` | Q.850 | 뜻 |
| --- | --- | --- |
| `softphone_not_registered` | 20 | 이 번호로 **등록된 단말이 없어** 연결하지 못했습니다. 단말의 등록 상태를 확인하세요. 등록되기를 기다렸다 넘겨 주지 않습니다. SIP 응답은 `480` 입니다(이 구분이 생기기 전 기록은 `404`, Q.850 `1` 로 남아 있습니다) |
| `sip_endpoint_not_found` | 1 · 20 | SIP 도착지가 `404`(또는 `604`)로 **대상을 찾을 수 없다고 응답**했습니다. 도착지 URI 와 받는 서버의 설정을 확인하세요. 도착지가 한국AI통신 SIP 도메인(`<계정>.sip.claw-ops.com`)이면 그 주소로 **등록된 단말이 없을 때**도 이 값이며, 이때 SIP 응답은 `480`, Q.850 은 `20` 입니다 |

단말이나 도착지가 받지 않거나 통화 중·거절로 응답하면 통신망과 같은 사유(`no_answer`·`user_busy`·`call_rejected` 등)로
기록됩니다. 벨이 울린 뒤 30초 안에 받지 않으면 `status` 는 `no-answer`, `hangupCause` 는 `no_answer` 입니다.

### 통화 처리 오류 — 번호 문제가 아닙니다

| `hangupCause` | `hangupSource` | 뜻 |
| --- | --- | --- |
| `app_error` | `app` | 통화를 처리하지 못했습니다. 웹훅 서버가 10초 안에 응답하지 않았거나 2xx 가 아닌 응답을 돌려준 경우, 응답한 VoiceML 을 해석할 수 없는 경우를 포함합니다 |
| `call_stuck` | `system` | 통화가 비정상 상태로 남아 시스템이 정리했습니다 |

`hangupSource` 가 **`app` 또는 `system`** 이면 상대방·번호 문제가 아닙니다. 수신자 번호를 정제 대상으로 넣지 마세요.
VoiceML 웹훅을 쓰는 번호의 `app_error` 는 웹훅 서버의 응답을 먼저 확인해 주세요. 그 밖의 `app_error` 나 `call_stuck` 이 반복되면 통화 ID 와 함께 문의해 주세요.

### 실패가 아닌 종료

| `hangupCause` | Q.850 | 뜻 |
| --- | --- | --- |
| `normal_clearing` | 16 | 통화 후 정상 종료 (`status: "completed"`). 누가 먼저 끊었는지는 `hangupSource` 로 구분합니다 |
| `caller_canceled` | 16 | 상대가 받기 전에 발신 측이 끊음 |
| `call_rejected` | 21 · 50 · 57 | 수신자가 거절 (수신 차단 포함). 수신 통화에서는 VoiceML `<Reject>` 로 거절한 통화도 이 값입니다 |
| `unspecified` | 31 · 127 | 통신망이 사유를 주지 않음 |
| `unknown` | — | 위 어디에도 해당하지 않는 cause |

## 사유로 조회하기

[통화 목록 조회](/docs/api/calls/listCalls) 의 `hangupCause` 로 필터링합니다.
`status` 는 `failed` 하나에 결번·망 오류·시스템 오류가 다 섞여 있어, 사유별로 보려면 이쪽을 쓰세요.

```
GET /v1/accounts/{accountId}/calls?hangupCause=concurrency_limit_exceeded
GET /v1/accounts/{accountId}/calls?hangupCause=invalid_number&hangupCause=number_changed
```

기간·통화시간과 함께 좁힐 수도 있습니다. `since`/`until` 은 시작 시각 기준이고 `until` 은 미포함,
`minDuration`/`maxDuration` 은 초 단위로 양끝을 포함합니다. 통화 시간이 아직 확정되지 않은
통화(진행 중 등)는 `minDuration`/`maxDuration` 을 주면 결과에서 빠집니다.

```
# 지난달 놓친 전화 중 0초로 끝난 것만
GET /v1/accounts/{accountId}/calls?since=2026-08-01T00:00:00Z&until=2026-09-01T00:00:00Z&minDuration=0&maxDuration=0
```

## `hangupSource` — 누구 때문에 끝났나

| 값 | 뜻 |
| --- | --- |
| `carrier` | 통신망이 통화를 끊었습니다 (결번·망 장애 등) |
| `callee` | 수신 측이 끝냈습니다 (거절·통화중·무응답, 또는 통화 후 먼저 끊음) |
| `caller` | 발신 측이 끝냈습니다 (연결 전 취소, 또는 통화 후 먼저 끊음) |
| `app` | 한국AI통신 통화 처리 오류 |
| `system` | 한국AI통신 시스템이 정리하거나 종료한 통화 |
| `null` | 종료 주체를 판정하지 못했습니다 |

통화 후 정상 종료(`normal_clearing`)에서 `caller`·`callee` 는 **먼저 끊은 쪽의 역할**입니다.
여러분의 앱·에이전트가 끊었다면 그 통화에서 맡은 역할로 기록됩니다.

| 통화 방향 | 상대가 먼저 끊음 | 여러분의 앱·에이전트가 먼저 끊음 |
| --- | --- | --- |
| 착신 (`inbound`) | `caller` | `callee` |
| 발신 (`outbound`) | `callee` | `caller` |

2026년 10월 12일부터 정상 종료 통화도 실제로 먼저 끊은 쪽을 기록합니다. 그 전에는 정상 종료 통화의
`hangupSource` 가 누가 끊었는지와 관계없이 항상 `callee` 였습니다.

## `sipResponseCode`

통신망이 보낸 SIP 응답코드가 있을 때만 채워집니다. 없이 끝난 통화는 `null` 입니다.

| 코드 | 흔한 의미 |
| --- | --- |
| `404` | 없는 번호 |
| `403` | 발신이 거부됨 |
| `408` | 응답 없음 |
| `480` | 일시적으로 연결할 수 없음 |
| `486` | 통화 중 |
| `500` | 통신망 내부 오류 — 실제 사유는 `hangupCause` 를 보세요 |

국내 통신망은 실제 사유를 `500` 으로 감싸 보내는 경우가 있습니다. 한국AI통신은 통신망이 함께 보낸
Q.850 원인값을 읽어 `hangupCause` 로 교정하므로, **SIP 코드보다 `hangupCause` 가 정확합니다.**

## 예제 — 결번 자동 정제

발신 후 통화 결과를 조회해, 다시 걸어도 소용없는 번호를 목록에서 빼는 예제입니다.

```python
DO_NOT_RETRY = {"invalid_number", "number_changed", "incompatible_destination"}

call = client.accounts(account_id).calls.get(call_id)

if call.status != "completed":
    cause = getattr(call, "hangup_cause", None)
    if cause in DO_NOT_RETRY:
        mark_number_invalid(call.to)          # 고객 DB 에서 제외
    elif getattr(call, "hangup_source", None) in ("app", "system"):
        alert_ops(call_id)                    # 번호 문제 아님 — 웹훅 서버·통화 처리 확인
    else:
        enqueue_retry(call.to, delay_min=30)  # 일시적 사유 — 나중에 재시도
```

Status Callback 으로 실시간 처리하려면 종료 이벤트에서 같은 값을 읽으면 됩니다.

```python
@app.route("/status", methods=["POST"])
def status_callback():
    if request.form.get("CallStatus") in ("completed", "failed", "busy", "no-answer"):
        cause = request.form.get("HangupCause")
        if cause in DO_NOT_RETRY:
            mark_number_invalid(request.form.get("To"))
    return "", 204
```

## 통화 전환(transfer)의 실패 사유

이 페이지의 `hangupCause` 는 **발신자와 한국AI통신 사이의 통화(메인 leg)** 에 대한 값입니다.
에이전트나 SIP 단말이 상담원에게 넘긴 **전환 leg** 는 별도 필드를 씁니다 —
`transferHangupCauseQ850` · `transferSipResponseCode` · `transferReasonText`,
그리고 다단계 전환의 전체 이력은 `transfers[]` 입니다.
값을 읽는 법은 [통화 전환 결과](/docs/call-transfers) 를 보세요.

VoiceML `<Dial>` 은 이 `transfer*` 필드를 채우지 않습니다. `<Dial>` 결과는 action 요청의
`DialCallStatus` 로 받습니다 — [VoiceML](/docs/build/voiceml) 참고.
