---
title: "telephony.incoming / web.incoming"
description: "수신 통화의 구성을 실시간으로 설정하는 차단 웹훅입니다."
---

수신 전화가 **할당된 에이전트가 없는
번호**에 도달하거나, 웹 위젯 세션이 `mode="webhook"`의 공개 가능 키에서 시작되면 ThunderPhone은
[레거시 웹훅 URL](/api-reference/organizations#legacy-single-url-webhook)로 **차단**
`telephony.incoming` / `web.incoming` 요청을 전송하고 구성 응답을 기다립니다. 전화의 경우 재시도 1회를 포함하여 최대 **13초**,
위젯 세션의 경우 최대 **10초** 동안 기다립니다([시간 초과 및 재시도](#timeouts-and-retries) 참조). 이 교환을 사용하여 통화별로 프롬프트, 음성 및 도구를 동적으로 선택합니다.
전체 패턴은 [동적 통화 구성 가이드](/ko/guides/dynamic-call-config)를 참조하세요.

<Note>
  구독한 [웹훅 엔드포인트](/ko/webhooks/endpoints)도
  `telephony.incoming` / `web.incoming`을 수신합니다. 에이전트 구성 여부와 관계없이 **모든**
  수신 전화 및 웹 세션에 대해 수신하지만, 이러한 전송은 `event_id`가 포함된 응답을 기다리지 않는
  알림이며 차단형이 아닙니다. 이 페이지의 구성 교환은 레거시 단일 URL 웹훅에서만 수행됩니다.
  엔드포인트 알림 형식은 [이벤트 카탈로그](/ko/webhooks/events)에 있습니다.
</Note>

## 시간 초과 및 재시도

신속히 응답하세요. 결정하는 동안 발신자는 회선에서 기다리고 있습니다.

전화 통화(`telephony.incoming`)의 경우:

- 첫 번째 시도는 최대 **10초**까지 걸릴 수 있습니다.
- 시간 초과, 연결 실패 또는 `5xx`, `408`, `429`
  응답이 발생하면 ThunderPhone이 한 번 더 시도합니다. 재시도에는 전체 교환에 대한 **13초**
  예산에서 남은 시간이 할당됩니다. 첫 번째 시도가 시간 초과된 후에는 약 3초,
  빠르게 실패한 경우에는 거의 13초가 남습니다.
  남은 시간이 2초 미만이면 재시도하지 않습니다.
- 그 외 `4xx` 응답, [크기 제한](#response-size-limit)을 초과하는 응답 또는 본문이
  유효한 구성이 아닌 `2xx` 응답은 최종 응답으로 처리되며 재시도하지 않습니다.
- 두 시도 모두 동일한 본문, 동일한 `X-ThunderPhone-Signature`,
  동일한 `X-ThunderPhone-Idempotency-Key` 헤더
  (`telephony-incoming-<call_id>`)를 전송하므로, 부작용이 있는 핸들러도
  재시도를 인식할 수 있습니다.

대체 에이전트는 없습니다. 사용 가능한 구성이 도착하지 않으면 발신자를 무음 상태의
회선에 남겨 두지 않고 통화를 즉시 종료합니다. 통화 기록에는 청구되지 않은 실패 통화로
표시되며 `end_reason`은 `inbound_config_webhook_failed`이고, 일반적인
[`telephony.complete`](/ko/webhooks/call-complete) 이벤트가 전송됩니다.

위젯 세션(`web.incoming`)에는 최대 10초의 단일 시도가 제공됩니다. 실패하면 세션 요청은
`502` 또는 `422`로 실패합니다.

<Warning>
  **웹훅으로 구성된 통화에는 ThunderPhone 동의 안내가 포함되지 않습니다.**
  이 교환을 통해 구성된 통화는 에이전트 수준의 통화 시작 안내를 우회하며
  ThunderPhone의 동의 안내 체계(서비스 약관의 "녹음 및 동의" 섹션)에서 명시적으로 제외됩니다.
  이러한 통화에 필요한 모든 녹음, 모니터링, AI 참여 및 발신자 식별 고지와 동의에 대한
  책임은 전적으로 조직에 있습니다. 통화는 여전히 녹음, 전사, 분석 및 AI 처리 대상이 될 수
  있습니다. 이 경로를 활성화하기 전에 필요한 고지를 자체 통화 흐름에 포함하세요.
</Warning>

## 요청 페이로드

전화 통화(`telephony.incoming`)의 경우:

```json
{
  "type": "telephony.incoming",
  "data": {
    "call_id":     987654321,
    "from_number": "+14155550199",
    "to_number":   "+15551234567"
  }
}
```

| 필드 | 유형 | 설명 |
|-------|------|-------------|
| `call_id` | integer | 통화 ID — 이 통화의 모든 이벤트에서 일관되게 유지됩니다 |
| `from_number` | string | E.164 발신자 번호 |
| `to_number` | string | E.164 대상 번호(ThunderPhone 번호 중 하나) |

웹 위젯 세션(`web.incoming`)의 경우 `data`는 전화번호 대신
삽입된 페이지를 식별합니다.

```json
{
  "type": "web.incoming",
  "data": {
    "call_id": 987654322,
    "origin_domain": "https://example.com",
    "publishable_key_prefix": "pk_live_a1b2"
  }
}
```

| 필드 | 유형 | 설명 |
|-------|------|-------------|
| `call_id` | 정수 | 통화 ID |
| `origin_domain` | 문자열 | 브라우저가 전송한 위젯을 호스팅하는 페이지의 출처입니다. 브라우저가 아닌 클라이언트는 어떤 값이든 보낼 수 있으므로, 요청의 출처를 증명하는 것으로 취급하지 마십시오 |
| `publishable_key_prefix` | 문자열 | 세션을 연 공개 키의 처음 몇 글자입니다 |
| `language`, `primary_language` | 문자열 | 위젯 세션에서 언어 재정의를 요청한 경우 존재합니다 |
| `voice` | 문자열 | 위젯 세션에서 음성 재정의를 요청한 경우 존재합니다 |
| `website_context` | 문자열 | 위젯이 세션별 페이지 컨텍스트를 전달한 경우 존재합니다 |

재정의 필드(`language`, `voice`, `website_context` 및 모든 브라우저 `variables`)는 공개 키의 [브라우저 재정의](/ko/guides/embed-web-widget)에서 허용하는 경우에만 전달됩니다. 키가 허용하지 않는 필드를 포함한 세션 요청은 이 웹훅이 호출되기 전에 거부됩니다.

<Note>
  웹훅 모드 위젯은 설정된 경우 공개 키 자체의 `webhook_url`로 이 요청을 전송하며,
  그렇지 않으면 조직 수준 웹훅 URL로 대체됩니다. 어느 경우든 조직 웹훅 `secret`으로 서명됩니다.
</Note>

---

## 응답 스키마

이 통화의 에이전트 구성을 설명하는 JSON 객체를 반환합니다.
`prompt` 및 `voice`는 필수이며, 그 외의 모든 항목은 선택 사항입니다.

```json
{
  "prompt":  "You are a helpful booking assistant for Acme Restaurant.",
  "voice":   "john",
  "product": "spark",
  "background_track": null,
  "tools":   []
}
```

| 필드 | 유형 | 필수 | 설명 |
|-------|------|----------|-------------|
| `prompt` | 문자열 | 예 | 에이전트를 구동하는 시스템 프롬프트 |
| `voice` | 문자열 | 예 | [`GET /v1/voices`](/api-reference/agents#voices)의 음성 ID(예: `john`)입니다. `voice_name`은 별칭으로 허용됩니다. 알 수 없는 음성은 유효성 검사에 실패하며 통화가 거부됩니다. |
| `product` | 문자열 | 아니요 | 기본값은 `spark`입니다. 허용되는 값: `spark`, `bolt`, `storm-base`, `storm-base-with-ack`, `storm-extra`, `storm-extra-with-ack` |
| `thinking_level` | 문자열 | 아니요 | `minimal`, `base`(기본값) 또는 `extra`입니다. Storm 제품에서는 재정의됩니다. `storm-extra*`는 `extra`를 강제하고, 다른 `storm-*`는 `base`를 강제합니다. |
| `audio_context_mode` | 문자열 | 아니요 | `full`(기본값) 또는 `reduced` |
| `watchdog_enabled` | 불리언 | 아니요 | 이 통화에 대한 감독을 활성화합니다. 기본값은 `false`입니다. |
| `additional_audio_context` | 불리언 \| null | 아니요 | 가장 최근 턴만 포함하는 대신 발신자 오디오의 최근 몇 개 턴을 포함하여, 약간의 지연 시간 및 비용 증가로 수정과 철자/숫자가 많은 데이터 수집을 개선합니다. 수신 세션에서는 기본적으로 켜지고 발신 전화 통화에서는 꺼집니다. `null`은 기본값을 유지합니다. |
| `storm_feedback_mode` | 문자열 | 아니요 | `none`, `acknowledgement`(기본값) 또는 `tick` |
| `language` | 문자열 | 아니요 | `primary_language`의 축약형 |
| `primary_language` | 문자열 | 아니요 | 정규화된 언어 코드(기본값 `en`)입니다. 확인할 수 없는 코드는 통화를 거부합니다. |
| `has_additional_languages` | 불리언 | 아니요 | 기본값은 `false`입니다. |
| `additional_languages` | 문자열 배열 | 아니요 | 에이전트가 전환할 수 있는 추가 언어 |
| `native_voice_switching` | 불리언 | 아니요 | 기본값은 `false`입니다. 통화가 다른 언어로 전환될 때 구성된 음성을 유지하는 대신 해당 언어의 원어민 음성으로 교체합니다(성별 일치). |
| `language_switching` | 문자열 \| null | 아니요 | `ask_first`(기본값) 또는 `follow_caller`입니다. `null`은 `ask_first`와 같습니다. 다국어 에이전트가 상대방이 지원 언어 중 다른 언어를 사용할 때 처리하는 방식입니다. `ask_first`는 상대방이 해당 언어를 요청하거나 그 언어로 완전한 문장을 말하면 즉시 전환하고, "예"처럼 짧은 발화 뒤에는 해당 언어로 계속할지 묻습니다. `follow_caller`는 한 단어만 말해도 상대방이 사용하는 구성된 언어로 전환합니다. `additional_languages`가 설정된 경우에만 적용됩니다. |
| `background_track` | 문자열 \| null | 아니요 | 배경 오디오 ID 또는 `null` |
| `acknowledgement_prompt_mode` | 문자열 | 아니요 | `auto`(기본값) 또는 `manual`(Storm-with-ack 제품) |
| `acknowledgement_prompt` | 문자열 | 아니요 | `acknowledgement_prompt_mode="manual"`일 때 사용됩니다. |
| `silence_interval_seconds` | 정수 \| null | 아니요 | 5–120입니다. 확인 안내 전 발신자 무음 시간(초)입니다. |
| `silence_max_checkins` | 정수 \| null | 아니요 | 1–10 |
| `silence_checkins_enabled` | 불리언 | 아니요 | 기본값은 `true`입니다. |
| `connect_tone_enabled` | 불리언 | 아니요 | 기본값은 `false`입니다. |
| `pronunciation_dicts` | 불리언 \| null | 아니요 | 기본값은 `false`이며, `null`은 `false`와 같습니다. `true`이면 통화의 Cartesia 음성은 해당 언어용 ThunderPhone 발음 사전이 있는 경우 이를 사용합니다. 전화 및 위젯 통화에서 작동합니다. 자체 Cartesia API 키의 음성이 아닌 ThunderPhone 자체 Cartesia 음성에만 적용됩니다. 저장된 에이전트 설정이 적용되는 `agent_id`와 함께 사용할 때는 사용되지 않습니다. |
| `voicemail_action` | 문자열 | 아니요 | `prompt`(기본값), `hangup` 또는 `message` |
| `voicemail_message` | 문자열 | 아니요 | `voicemail_action="message"`일 때 사용됩니다. |
| `agent_name` | 문자열 | 아니요 | 대시보드와 위젯에 표시되는 이름 |
| `org_name` | 문자열 | 아니요 | 에이전트 페르소나의 조직 표시 이름 |
| `tools` | 배열 | 아니요 | 인라인 함수 도구 스키마([함수 도구](/ko/tools/overview) 참조) |
| `transfer_destinations` | 객체 배열 \| null | 아니요 | 이 통화에서 전환할 수 있는 최대 50개의 `{name, number}` 항목입니다. 에이전트의 [전환 번호](/api-reference/agents#transfer-numbers)와 동일하게 유효성을 검사합니다. 에이전트는 이름으로, 그리고 이 번호로만 전환합니다. `[]`는 전환이 없음을 의미합니다. 에이전트가 선택한 번호로 전환하도록 하려면 이를 생략하거나 `null`을 전송합니다. |
| `timezone` | 문자열 | 아니요 | `America/Chicago`와 같은 이 통화의 IANA 시간대입니다. 에이전트는 자체 시간대보다 이를 우선하여 날짜와 시간을 말합니다. `agent_id`와 함께 사용할 수도 있습니다. IANA 시간대가 아닌 값은 무시됩니다. [통화별 시간대 설정](/ko/guides/dynamic-call-config)을 참조하세요. |
| `call_id` | 정수 | 아니요 | 요청의 통화 ID를 선택적으로 그대로 반환하며, 무시됩니다. |

<Note>
  알 수 없는 최상위 키는 자동으로 **무시됩니다**. 필드 이름에 오타가 있어도
  구성은 거부되지 않지만 적용되지 않습니다. 발화 순서 및
  `max_hold_seconds`는 여기에서 허용되지 않으며,
  [에이전트](/api-reference/agents) 자체에서만 구성할 수 있습니다.
</Note>

`prompt`와 `voice`는 필수이므로, `{}` 또는 유효성 검사에 실패하는 응답을 반환하면 통화가 거부됩니다. 전화 통화는 [시간 초과 및 재시도](#timeouts-and-retries)에 설명된 대로 종료되고, 위젯 세션 요청은 `422`로 실패합니다. 이 경로에서는 정적 에이전트 대체가 제공되지 않습니다(웹훅 모드의 번호 또는 키에는 할당된 에이전트가 없습니다).

---

## 응답 크기 제한

<Warning>
  구성 응답은 **5 MiB**로 제한됩니다. 핸들러가 `2xx` 상태를 포함하여 이보다 큰 응답을 반환하면 ThunderPhone은 응답이 제한을 초과했다고 보고하고 통화 또는 위젯 세션을 거부합니다. 응답에는 통화 설정에 필요한 필드만 포함하세요. 큰 데이터는 구성에 포함하지 말고 함수 도구 또는 다른 서비스 뒤에 호스팅하세요.
</Warning>

---

## 핸들러 예시

<CodeGroup>
```python Python (FastAPI)
import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
WEBHOOK_SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]

def verify(body: bytes, signature: str) -> bool:
    expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

@app.post("/thunderphone-webhook")
async def webhook(request: Request):
    body = await request.body()
    if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
        raise HTTPException(status_code=401)

    event = json.loads(body)
    if event["type"] == "telephony.incoming":
        caller = event["data"]["from_number"]
        prompt = (
            "Greet the caller as a San Francisco local…"
            if caller.startswith("+1415")
            else "You are a friendly customer support agent…"
        )
        return {
            "prompt": prompt,
            "voice": "john",
            "product": "spark",
        }
    if event["type"] == "web.incoming":
        return {
            "prompt": "You are the website's helpful voice assistant…",
            "voice": "john",
            "product": "spark",
        }
    return {}
```

```javascript Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;

function verify(body, signature) {
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(body)
    .digest("hex");
  return signature &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));

    if (event.type === "telephony.incoming" || event.type === "web.incoming") {
      const caller = event.data.from_number || "web";
      const prompt = caller.startsWith("+1415")
        ? "Greet the caller as a San Francisco local…"
        : "You are a friendly customer support agent…";
      return res.json({
        prompt,
        voice: "john",
        product: "spark",
      });
    }
    res.json({});
  },
);
```
</CodeGroup>

---

## 함수 도구가 포함된 응답

대화 중 AI가 API를 호출할 수 있도록 도구를 연결하세요.

```json
{
  "prompt":  "You are a booking assistant. Use the available tools to help customers schedule appointments.",
  "voice":   "john",
  "product": "spark",
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "search_appointments",
        "description": "Find available appointment slots",
        "parameters": {
          "type": "object",
          "properties": {
            "date": { "type": "string", "description": "YYYY-MM-DD" },
            "service": { "type": "string" }
          },
          "required": ["date"]
        }
      },
      "endpoint": {
        "url": "https://api.example.com/appointments/search",
        "method": "POST",
        "headers": {
          "X-Api-Key": "your-key"
        }
      }
    }
  ]
}
```

<Tip>
  도구 엔드포인트 요청은 이 교환에 서명한 것과 **동일한 조직 웹훅 시크릿**으로 서명됩니다. 정확한 형식과 서명된 요청 형식은 [함수 도구](/ko/tools/overview)를 참조하세요.
</Tip>

---

## 제품 티어 요약

| 제품 | 지연 시간 | 추론 | 응답 확인 |
|---------|---------|-----------|-----------------|
| `spark` | 최저 | 기본 | — |
| `bolt` | 낮음 | 향상됨 | — |
| `storm-base` | 중간 | 강력함 | — |
| `storm-base-with-ack` | 중간 | 강력함 | 사고 중 자동 채움 응답 |
| `storm-extra` | 높음 | 심층 | — |
| `storm-extra-with-ack` | 높음 | 심층 | 사고 중 자동 채움 응답 |

---

## 관련 항목

<CardGroup cols={2}>
  <Card title="telephony.complete / web.complete" icon="phone-slash" href="/ko/webhooks/call-complete">
    차단 없이 전달되는 통화 종료 이벤트입니다.
  </Card>
  <Card title="함수 도구" icon="screwdriver-wrench" href="/ko/tools/overview">
    `tools[]`의 전체 JSON 스키마와 서명된 엔드포인트 계약입니다.
  </Card>
  <Card title="웹훅 엔드포인트" icon="bolt" href="/ko/webhooks/endpoints">
    여러 URL을 `telephony.incoming` / `web.incoming`에 구독합니다.
  </Card>
  <Card title="동적 통화 구성" icon="wand-magic-sparkles" href="/ko/guides/dynamic-call-config">
    발신자별 프롬프트, 도구 및 A/B 테스트를 위한 패턴입니다.
  </Card>
</CardGroup>
