# 걸려온 통화의 발신자 번호로 이어 걸기 (CallToken) (/docs/call-token)

## 걸려온 통화의 발신자 번호로 이어 걸기 (CallToken)

걸려온 통화가 이어지는 동안, 그 통화의 발신자 번호가 상대에게 표시되는 통화를 겁니다. Twilio `callToken` 과 같은 방식입니다.

  **걸려온 전화를 넘기는 착신전환입니다.** 받는 쪽에는 걸려온 통화의 발신자 번호가 표시되지만, 발신번호를 그 번호로 바꿔 새로 거는 것이 아닙니다. 통신사에는 그 전화를 받은 번호(예: 070 번호)에서 전환된 통화로 전달되므로, 받는 장비에 따라 그 번호가 표시될 수도 있습니다.

## 1. 걸려온 통화에서 CallToken 받기

걸려온 통화의 [웹훅 요청](/docs/build/voiceml)에 `CallToken` 이 실립니다.

```
CallId=CA7f3a9c0e21
From=01012345678
To=07012340001
Direction=inbound
CallToken=ctk_Q2FsbFRva2VuRXhhbXBsZTAxMjM0NTY3
```

## 2. 이어서 통화 걸기

`CallToken` 을 넣고, `From` 에는 걸려온 통화의 발신자 번호를 넣습니다. 이 값은 **받는 쪽에 어떤 번호를 표시할지**를 정합니다.

```bash
curl -X POST https://api.claw-ops.com/v1/accounts/AC1a2b3c4d/calls \
  -H "Authorization: Bearer $CLAWOPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "To": "01098765432",
    "From": "01012345678",
    "CallToken": "ctk_Q2FsbFRva2VuRXhhbXBsZTAxMjM0NTY3",
    "Url": "https://my-app.com/agent-leg"
  }'
```

이어 건 통화는 한국AI통신 기록·과금에서 걸려온 통화와 **연결되지 않은 별개의 통화**입니다. 두 통화를 잇는 것은 직접 구성합니다. 예를 들어 두 통화를 각각 `<Connect><Stream>` 으로 같은 서버에 붙여 오디오를 주고받습니다.

## 3. 이어 건 통화 끊기

상대가 거절했거나 더 필요 없으면 이어 건 통화만 끊습니다. 벨이 울리는 중이면 연결 전에 취소됩니다. 걸려온 통화는 그대로 유지됩니다.

```bash
curl -X POST https://api.claw-ops.com/v1/accounts/AC1a2b3c4d/calls/CA9b10d44d2 \
  -H "Authorization: Bearer $CLAWOPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "Status": "completed" }'
```

## SIP 트렁크에서 쓰기

착신을 SIP 로 직접 받는 번호는 웹훅 대신 착신 INVITE 의 `X-ClawOps-CallToken` 헤더로 CallToken 을 받습니다. Twilio `X-Twilio-CallToken` 과 같은 방식입니다.

```
INVITE sip:07012340001@sbc.example.com SIP/2.0
From: <sip:01012345678@sip.claw-ops.com>;tag=...
X-ClawOps-CallToken: ctk_Q2FsbFRva2VuRXhhbXBsZTAxMjM0NTY3
```

이어 걸 때는 SIP 트렁크 발신 INVITE 의 From 에 걸려온 통화의 발신자 번호를 넣고, 받은 헤더를 그대로 실어 보냅니다.

```
INVITE sip:01098765432@ac1a2b3c4d.sip.claw-ops.com SIP/2.0
From: <sip:01012345678@ac1a2b3c4d.sip.claw-ops.com>;tag=...
X-ClawOps-CallToken: ctk_Q2FsbFRva2VuRXhhbXBsZTAxMjM0NTY3
```

LiveKit 을 쓰면 인바운드 트렁크의 `headers_to_attributes` 로 헤더를 참가자 속성으로 받고, `CreateSIPParticipant` 의 `headers` 로 돌려보냅니다. 속성은 벨이 울리는 단계부터 읽을 수 있습니다.

```python
from livekit import api

# 1. 인바운드 트렁크 — X-ClawOps-CallToken 을 참가자 속성 clawops.callToken 으로
await lk.sip.create_inbound_trunk(api.CreateSIPInboundTrunkRequest(
    trunk=api.SIPInboundTrunkInfo(
        name="clawops-inbound",
        numbers=["07012340001"],
        headers_to_attributes={"X-ClawOps-CallToken": "clawops.callToken"},
    ),
))

# 2. 걸려온 통화의 참가자에서 토큰과 발신자 번호를 읽어 상담원에게 이어 걸기
caller = ...  # 방에 들어온 SIP 참가자
await lk.sip.create_sip_participant(api.CreateSIPParticipantRequest(
    sip_trunk_id=OUTBOUND_TRUNK_ID,
    sip_call_to="01098765432",
    sip_number=caller.attributes["sip.phoneNumber"],   # 걸려온 통화의 발신자 번호
    room_name=room,
    participant_identity="agent",
    headers={"X-ClawOps-CallToken": caller.attributes["clawops.callToken"]},
))
```

SIP 발신에서 CallToken 을 쓸 수 없으면 다음 응답으로 거절합니다.

| 응답 | `Reason` | 뜻 |
| --- | --- | --- |
| 403 | — | CallToken 없이 계정 번호가 아닌 From 으로 걸었음 |
| 403 | `Q.850;cause=21;text="call token invalid"` | 유효하지 않은 CallToken |
| 403 | `Q.850;cause=21;text="call token expired"` | 걸려온 통화가 이미 끊김 |
| 403 | `Q.850;cause=21;text="from does not match call token"` | From 이 걸려온 통화의 발신자 번호와 다름 |

## 사용 조건

- `CallToken` 은 **통신사망으로 직접 걸려온 통화**이면서 **발신자가 국내 번호**일 때만 실립니다. 그 외에는 파라미터가 오지 않습니다.
- 번호의 수신 처리가 **Voice URL(웹훅)** 일 때만 실립니다. SIP 트렁크에서는 착신을 SIP 로 직접 받는 번호에만 실립니다.
- SIP 트렁크 자격증명에 발신번호를 지정했으면 SIP 발신에서 CallToken 을 쓸 수 없습니다.
- 걸려온 통화가 **끊기면 더는 쓸 수 없습니다.**
- 이어 건 통화도 계정의 동시통화 수에 들어갑니다. 걸려온 통화와 함께 2통이 동시에 진행됩니다.
- REST 발신은 `Url` 로 거는 통화에만 쓸 수 있습니다. 수신 번호는 국내 번호만 가능합니다.

## 오류

| 상태 | `code` | 뜻 |
| --- | --- | --- |
| 400 | `call_token_invalid` | 유효하지 않은 CallToken |
| 400 | `call_token_from_mismatch` | `From` 이 걸려온 통화의 발신자 번호와 다름 |
| 400 | `call_token_mode_unsupported` | `Url` 없이 사용함 |
| 409 | `call_token_expired` | 걸려온 통화가 이미 끊김 |
