# 통화 전환 결과 (Transfer) (/docs/call-transfers)

## 통화 전환 결과 (Transfer)

통화를 상담원이나 다른 번호로 넘겼을 때, 그 전환이 어떻게 됐는지가 통화 조회 응답의
`transferTo` · `transferStatus` · `transferDuration` 에 남습니다.

  **이 필드들은 읽기 전용 관측 필드입니다.** 통화 조회 요청에 값을 실어 전환을 지정할 수는 없습니다.
  전환은 따로 실행하고, 그 결과가 이 필드들에 사후에 채워집니다. 전환이 없었던 통화는 전부 `null`
  입니다.

전환을 **거는** 방법은 [전환 걸기](#전환-걸기) 를 보세요.

## 어디서 받나요

| 경로                                             | 필드                                                                                                                                                    |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [통화 단건 조회](/docs/api/calls/getCall)        | `transferTo` · `transferStatus` · `transferDuration` · `transferHangupCauseQ850` · `transferSipResponseCode` · `transferReasonText` · **`transfers[]`** |
| [통화 목록 조회](/docs/api/calls/listCalls)      | 위와 동일 (통화마다) — **`transfers[]` 는 제외**                                                                                                        |
| [Status Callback webhook](/docs/webhooks/status) | `TransferStatus` · `TransferTo` · `TransferMode` · `TransferDuration` 등 (`transfer` 이벤트, 실시간)                                                    |

## 어느 전환이 여기에 기록되나요

| 전환 방법                                                                             | 위 필드에 기록되나                                                           |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| [Voice Agent SDK](/docs/voice-agent) 의 `transfer()` · 내장 도구 `transfer_call`      | ✅                                                                           |
| 콘솔에서 만든 에이전트의 **도구 → 통화 전환**                                         | ✅                                                                           |
| SIP 단말의 **REFER** 전환 (blind / attended)                                          | ✅                                                                           |
| [통화 전환 API](/docs/api/calls/transferCall) `POST /calls/{callId}/actions/transfer` | ✅                                                                           |
| VoiceML `<Dial>`                                                                      | ❌ — 아래 참고                                                               |
| VoiceML `<Dial referUrl>` 로 받은 SIP REFER                                           | ❌ — 전환이 아니라 **새 흐름**입니다. [`referUrl`](/docs/build/voiceml) 참고 |

## 전환 걸기

진행 중인 통화를 다른 번호나 SIP 엔드포인트로 넘깁니다.

```bash
curl -X POST "https://api.claw-ops.com/v1/accounts/{accountId}/calls/{callId}/actions/transfer" \
  -H "Authorization: Bearer $CLAWOPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "0212345678",
    "mode": "warm",
    "whisper": "예약 변경 문의입니다.",
    "callerIdMode": "original",
    "requestId": "req_9f3c21"
  }'
```

```json
{ "callId": "CA...", "transferSequence": 1, "status": "initiated", "requestId": "req_9f3c21" }
```

**접수만 하고 즉시 응답합니다.** 대상이 받을 때까지 기다리지 않습니다. 진행과 결과는
[Status Callback](/docs/webhooks/status) 의 `transfer` 이벤트로 오고, 응답의
`transferSequence` 가 그 결과를 아래 `transfers[]` 에서 찾는 키입니다.

**Voice Agent SDK 또는 콘솔 에이전트가 진행 중인 통화에만 사용할 수 있습니다.** VoiceML
(`<Dial>` 등)로 진행 중인 통화는 409 `TRANSFER_UNSUPPORTED` 로 거절됩니다 — 그쪽은
`<Dial>` 로 목적지를 붙이거나, SIP 단말이 보내는 REFER 를
[`referUrl`](/docs/build/voiceml) 로 받아 처리하세요.

### 주요 파라미터

- `to` (string, 필수): 전환 대상. destinationType 이 pstn(기본)이면 전화번호, sip 이면 SIP URI
- `mode` (string, 선택): blind(기본) = 받으면 바로 연결 / warm = whisper 안내를 대상에게만 들려준 뒤 연결
- `whisper` (string, 선택): warm 일 때 대상에게만 들려줄 안내. 발신자에게는 들리지 않습니다
- `callerIdMode` (string, 선택): original 을 주면 걸려 온 전화의 발신자 번호가 대상 단말에 표시됩니다. 통신사망으로 직접 걸려 온 통화이고 그 발신자가 국내 번호인 경우에만 성립하며, 성립하지 않으면 계정 번호로 표시합니다. 받는 쪽에 표시되는 번호만 바뀌는 것이고 통화 자체는 계정 번호에서 넘기는 전환이라, 받는 장비에 따라 계정 번호가 표시될 수도 있습니다
- `afterTransfer` (string, 선택): return(기본) = 전환이 끝나도 통화를 유지하고 에이전트가 이어받습니다 / terminate = 통화를 종료합니다
- `requestId` (string, 선택): 같은 값의 재요청은 새 전환을 만들지 않습니다(멱등). 전환 webhook 에 RequestId 로 되돌아옵니다. 255자 이하

전체 파라미터와 오류 코드는 [API 레퍼런스](/docs/api/calls/transferCall) 에 있습니다.

### 전환이 실패하면

대상이 받지 않거나 통화 중이면 **발신자와의 통화는 그대로 유지됩니다**(`afterTransfer` 기본값
`return`). 다른 대상으로 다시 걸 수 있습니다.

다만 이어받을 에이전트가 전환 결과를 받을 수 없는 통화 — 콘솔에서 만든 에이전트이거나,
에이전트 연결이 끊긴 경우 — 에서는 `return` 이 성립하지 않아 통화가 종료됩니다. 전환이 있었다는
사실을 모르는 에이전트가 맥락 없이 대화를 잇게 되기 때문입니다. 그때는 통화 이벤트에
`transfer.after_transfer_downgraded` 가 남습니다.

Trial 에서 월 발신 통화 포함량을 다 쓴 계정은 전환이 시작되지 않고 `422 quota_exceeded`
로 거절됩니다. 이때도 발신자와의 통화는 유지됩니다 — [요금](/docs/api-reference/pricing) 참고.

### 재시도

응답이 지연돼 503 `transfer_timeout` 을 받은 경우, **전환은 진행 중일 수 있습니다.** 같은
`requestId` 로 재시도하면 중복 전환 없이 먼저 접수된 전환의 `transferSequence` 를 돌려줍니다.

## `<Dial>` 은 다릅니다

VoiceML 의 [`<Dial>`](/docs/build/voiceml) 은 통화 중에 다른 목적지를 붙이는 것이지, 통화를 넘기고 빠지는 전환이
아닙니다. 그래서 위 `transfer*` 필드에는 아무것도 남지 않습니다. 결과는 두 가지로 받습니다.

| 무엇을 알고 싶은가       | 어디서 받나                                                                                          |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| 연결이 **어떻게 끝났나** | `<Dial action>` 요청의 `DialCallStatus` (`completed` / `busy` / `no-answer` / `failed`) |
| 상대가 **언제 받았나**   | `<Number statusCallback>` / `<Sip statusCallback>` 통지 (`answered` · `completed`)                   |

## 필드

- `transferTo` (string | null, 선택): 전환 대상 번호 또는 SIP URI. 전환이 없었으면 null
- `transferStatus` (string | null, 선택): 전환 결과 — completed / no-answer / busy / canceled / failed. 아래 값 표 참고
- `transferDuration` (number | null, 선택): 전환 통화 시간(초). 대상이 받은 시점부터 그 전환이 끝날 때까지. 연결되지 못했으면 null
- `transferHangupCauseQ850` (number | null, 선택): 전환 leg 의 통신망 Q.850 cause. 사유 미상이면 null
- `transferSipResponseCode` (number | null, 선택): 전환 leg 의 SIP 응답코드 (예: 486, 500). 사유 미상이면 null
- `transferReasonText` (string | null, 선택): 통신망이 준 원본 진단 텍스트. 자유 형식이라 분기 조건으로 쓰지 마세요
- `transfers` (array, 선택): 전환 leg 전체 이력. 단건 조회에만 실립니다 — 목록 조회 응답에는 없습니다

실패 사유 세 필드(`transferHangupCauseQ850` · `transferSipResponseCode` · `transferReasonText`)의
값 해석은 [통화 실패 사유](/docs/call-failure-causes) 와 같습니다. 다만 그 페이지의 `hangupCause`
는 **발신자와 한국AI통신 사이의 통화(메인 leg)** 에 대한 값이고, 여기 `transfer*` 는 **전환 leg** 에
대한 값입니다.

## `transferStatus` 값

| 값          | 뜻                                                                                 |
| ----------- | ---------------------------------------------------------------------------------- |
| `completed` | 대상이 받아 통화가 이뤄졌고 정상 종료됐습니다                                      |
| `no-answer` | 벨은 울렸으나 받지 않았습니다                                                      |
| `busy`      | 대상이 통화 중입니다                                                               |
| `canceled`  | 연결되기 전에 취소·중단됐습니다 (걸려 온 통화의 발신자가 먼저 끊는 경우 포함)      |
| `failed`    | 그 외 실패. 사유는 `transferHangupCauseQ850` · `transferSipResponseCode` 로 봅니다 |

  **진행 중 상태는 이 필드에 나타나지 않습니다.** `transferStatus` 는 끝난 전환의 결과만 담습니다.
  `initiated`(시작) · `ringing` · `connected`(연결됨) · `interrupted`(통화가 비정상 종료돼 시스템이
  정리) 는 `transfers[].status` 와 Status Callback 의 `transfer` 이벤트에만 존재합니다.

## 다단계·재시도 전환

한 통화에서 전환을 여러 번 시도할 수 있습니다 — 1번 상담원이 안 받아 2번으로 넘기는 식입니다.
이때 `transfers[]` 에는 시도마다 한 건씩 쌓이고, 위 `transfer*` 단일 필드는 그중 **대표 leg 하나**만
보여줍니다.

대표 leg 는 **끝난 leg 중 하나**입니다 — `completed` 인 leg 가 있으면 그것을, 없으면 `sequence` 가
가장 큰 leg 를 보여줍니다.

즉 세 번 시도해 마지막에 성공했다면 `transferStatus` 는 `completed`, `transferTo` 는 그 성공한
번호이고, 앞선 두 번의 실패는 `transfers[]` 에만 남습니다. **전환 시도를 빠짐없이 보려면
`transfers[]` 를 쓰세요.**

- `sequence` (number, 필수): 이 통화에서 몇 번째 전환 시도인지 (1부터)
- `to` (string | null, 선택): 전환 대상
- `mode` (string | null, 선택): blind(즉시 전환) / warm(대상에게 whisper 안내 후 연결) / refer, refer-attended(SIP 단말이 건 REFER 전환)
- `destinationType` (string | null, 선택): pstn(통신망 번호) / sip(SIP URI 직결). 2026-07-10 이전 전환은 null
- `status` (string | null, 선택): initiated / ringing / connected / completed / failed / no-answer / busy / canceled / interrupted
- `duration` (number | null, 선택): 연결→종료 초(관측용). 연결되지 못했으면 null
- `billable` (boolean, 필수): 과금 대상 여부. 한 통화에서 최대 한 건만 true 입니다
- `billableDuration` (number | null, 선택): 과금 대상 초
- `hangupCauseQ850` (number | null, 선택): 이 leg 의 통신망 Q.850 cause
- `sipResponseCode` (number | null, 선택): 이 leg 의 SIP 응답코드
- `reasonText` (string | null, 선택): 통신망 원본 진단 텍스트
- `startedAt` (string | null, 선택): 전환을 시작한 시각 (ISO 8601). 2026-07-10 이전 전환은 null
- `answeredAt` (string | null, 선택): 대상이 전화를 받은 시각. 응답까지 걸린 시간은 answeredAt - startedAt 입니다. 연결 전 안내(warm) 도중 끝나도 남습니다. 받지 않았으면 null (⚠️ 2026-10-13 이전 전환은 받았어도 null — 아래 참고)
- `connectedAt` (string | null, 선택): 발신자와 대상이 연결된 시각. warm 전환은 안내가 끝난 뒤 시각이라 answeredAt 보다 늦습니다. 연결되지 못했으면 null (⚠️ 2026-07-10 이전 전환은 연결됐어도 null — 아래 참고)
- `endedAt` (string | null, 선택): 이 leg 가 끝난 시각. 2026-07-10 이전 전환은 null
- `timeout` (number | null, 선택): 이 전환에 적용한 대기 한도(초). 2026-10-13 이전 전환은 null

  **담당자가 받기까지 걸린 시간은 `answeredAt - startedAt` 입니다.** `connectedAt` 으로 계산하면
  warm 전환에서 안내 길이만큼 길게 나옵니다.

**받았는지는 `answeredAt` 으로 판정하세요.** 값이 있으면 받은 전환입니다 — 연결 전 안내 도중 끝난
warm 전환은 `status` 가 `failed` · `canceled` · `no-answer` 여도 담당자는 받았습니다. `answeredAt` 이
없으면 `connectedAt` 이 있거나 `status` 가 `completed` 일 때만 받은 것입니다.

`answeredAt` · `timeout` 은 2026-10-13 부터 기록돼, 그 이전 전환은 `null` 이고 안내 도중 끝난 경우를
구분할 수 없습니다.

  **2026-07-10 이전 전환은 leg 별 시각과 `destinationType` 이 없습니다.** 그때는 전환 결과를 통화 한
  건에 한 줄로만 기록했고, 지금의 leg 이력은 그 기록에서 되살린 것이라 `startedAt` · `connectedAt` ·
  `endedAt` · `destinationType` 이 전부 `null` 입니다. **연결 여부를 `connectedAt` 으로 판정하면 그
  구간을 오분류합니다** — `status` 와 `duration` 을 보세요(그 둘은 레거시 전환에도 있습니다).

  `transferDuration` 과 `transfers[].duration` 은 **관측값이지 과금 초가 아닙니다.** 청구 기준 초는
  `transfers[].billableDuration` 이니 `duration` 을 합산하지 마세요. 합산·올림 규칙은 [사용량
  조회](/docs/api-reference/usage) 를 보세요.

## 예시

전환을 두 번 시도해 두 번째에 연결된 통화입니다.

```json
{
  "callId": "CAabcdef1234567890",
  "status": "completed",
  "from": "01012345678",
  "to": "07080588491",
  "direction": "inbound",
  "duration": 184,
  "hangupCause": "normal_clearing",
  "transferTo": "0212345679",
  "transferStatus": "completed",
  "transferDuration": 96,
  "transferHangupCauseQ850": 16,
  "transferSipResponseCode": null,
  "transferReasonText": null,
  "transfers": [
    {
      "sequence": 1,
      "to": "0212345678",
      "mode": "blind",
      "destinationType": "pstn",
      "status": "no-answer",
      "duration": null,
      "billable": false,
      "billableDuration": null,
      "hangupCauseQ850": 19,
      "sipResponseCode": 480,
      "reasonText": null,
      "startedAt": "2026-10-14T04:11:02.000Z",
      "answeredAt": null,
      "connectedAt": null,
      "endedAt": "2026-10-14T04:11:32.000Z",
      "timeout": 30
    },
    {
      "sequence": 2,
      "to": "0212345679",
      "mode": "blind",
      "destinationType": "pstn",
      "status": "completed",
      "duration": 96,
      "billable": true,
      "billableDuration": 96,
      "hangupCauseQ850": 16,
      "sipResponseCode": null,
      "reasonText": null,
      "startedAt": "2026-10-14T04:11:33.000Z",
      "answeredAt": "2026-10-14T04:11:41.000Z",
      "connectedAt": "2026-10-14T04:11:41.000Z",
      "endedAt": "2026-10-14T04:13:17.000Z",
      "timeout": 30
    }
  ]
}
```

조회는 통화 ID 로 합니다.

```bash
curl "https://api.claw-ops.com/v1/accounts/AC1a2b3c4d/calls/CAabcdef1234567890" \
  -H "Authorization: Bearer sk_..."
```

## 통화가 끝나기 전에 알기

위 필드는 통화가 끝난 뒤에 조회해서 봅니다. 전환이 실패했을 때 **바로** 다른 상담원으로 재시도하거나
안내 문자를 보내려면 [Status Callback](/docs/webhooks/status) 의 `transfer` 이벤트를 쓰세요.
전환 한 건마다 시작 → 연결 → 종료 순으로 통지됩니다.

`transfer` 는 **기본 이벤트에 포함되지 않습니다.** 받으려면 기본 이벤트와 함께 직접 나열해야 합니다.

```
initiated ringing answered completed transfer
```


