---
title: "통화별 동적 구성"
description: "직접 제어하는 웹훅의 맞춤 로직을 기반으로, 수신 통화마다 응답 에이전트를 선택하거나 프롬프트와 설정을 개별적으로 다시 구성합니다."
---

기본적으로 모든 전화번호와 공개 키에는 정적으로 할당된 에이전트가 있습니다. **발신자별** 또는 **방문자별** 맞춤 설정이 필요한 경우 — VIP 라우팅, 로그인한 사용자 컨텍스트, A/B 프롬프트 테스트 — 웹훅 모드로 전환하고 서버가 결정하도록 하십시오.

## 작동 방식

1. [`telephony.incoming`](/ko/webhooks/events)
   (전화) 또는 [`web.incoming`](/ko/webhooks/events) (위젯)
   이벤트를 구독합니다. 둘 다 **차단형** 웹훅입니다. ThunderPhone은
   통화를 계속하기 전에 응답을 기다리며, 전화 통화의 경우 최대 13초
   (재시도 1회 포함), 위젯 세션의 경우 최대 10초 동안 기다립니다.
2. ThunderPhone이 `{call_id, from_number, to_number}`를 전송합니다(위젯
   세션은 번호 대신 위젯별 필드를 포함합니다. [요청 스키마](/ko/webhooks/call-incoming)를
   참조하세요).
3. 서버가 에이전트 구성(프롬프트, 음성, 제품, 도구)으로 응답합니다.
   ThunderPhone은 해당 구성을 통화에 사용합니다.
4. 대체 에이전트는 없습니다. `{}` 또는 잘못된 구성을 반환하거나,
   [재시도](/ko/webhooks/call-incoming) 후에도 요청이
   실패하면 전화 통화는 즉시 종료되고 `end_reason`
   `inbound_config_webhook_failed`로 기록됩니다. 위젯 세션 요청은 실패합니다.

<Note>
  웹훅 엔드포인트 또는 레거시 단일 URL 웹훅으로 전달되는지와 관계없이 전화 통화
  (`telephony.incoming`) 및 위젯 세션 (`web.incoming`)에 동일하게 작동합니다.
</Note>

<Warning>
  **인라인 웹훅으로 구성된 통화에서는 ThunderPhone 동의 고지가 재생되지
  않습니다.** 이 경로는 에이전트 수준의 통화 시작 고지를 우회하며
  ThunderPhone의 동의 고지 프레임워크(서비스 약관의 "녹음 및 동의" 섹션)에서
  명시적으로 제외됩니다. 이러한 통화의 모든 녹음, 모니터링, AI 참여 및
  발신자 식별 고지와 동의에 대한 책임은 조직에 있습니다. 인라인 웹훅 모드를
  활성화하기 전에 자체 흐름에서 이를 제공하십시오. 예를 들어 프롬프트의
  시작 스크립트에 포함할 수 있습니다. 저장된 `agent_id`를 참조하는 웹훅 응답은
  해당 에이전트의 일반 녹음 및 고지 정책을 사용합니다.
</Warning>

## 1. 웹훅 대상 구성

<Tabs>
<Tab title="전화 통화">
전화번호의 경우 엔드포인트가 `telephony.incoming`을 구독하도록 구성합니다.

```bash
curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label":  "Prod call-incoming",
    "url":    "https://example.com/thunderphone/incoming",
    "events": ["telephony.incoming"]
  }'
```

응답에는 일회성 `secret`이 포함됩니다. 저장하십시오. 서명 검증에 사용합니다.
</Tab>
<Tab title="웹 위젯">
위젯 세션의 경우 엔드포인트 URL이 포함된 `mode="webhook"` 공개 키를 생성합니다.

```bash
curl -X POST https://api.thunderphone.com/v1/publishable-key \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name":            "Dynamic widget",
    "mode":            "webhook",
    "webhook_url":     "https://example.com/thunderphone/widget-incoming",
    "allowed_domains": ["example.com"]
  }'
```

위젯은 세션이 시작될 때마다 이 URL로 POST 요청을 보냅니다.
</Tab>
</Tabs>

## 2. 핸들러 구현

세 가지 경험칙:

- 모든 요청에서 **서명을 검증합니다**( [웹훅 서명 검증](/ko/guides/verify-webhook-signatures) 참고).
  개발 환경에서도 이를 건너뛰지 마세요. 한 번 올바르게 구현하고 재사용하세요.
- **빠르게 응답합니다**. 전화 통화의 첫 시도는 10초에, 전체 교환은 13초에
  종료되며, 매초가 발신자에게는 무음 시간입니다. 필요하면 데이터베이스를 조회하되, 후속 LLM을 동기적으로 호출하지 마세요. 동적 프롬프트 생성이 필요하다면 미리 계산하고 캐시하세요.
- **정상적으로 대체 처리합니다**. 예상하지 못한 상태에서는 기본 `prompt`와 `voice`(또는 저장된 `agent_id`)로
  응답하세요. `{}` 또는 오류 상태를 반환하면 통화가 종료됩니다.

<CodeGroup>
```python FastAPI
import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

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

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

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

    event = json.loads(body)
    if event["type"] not in ("telephony.incoming", "web.incoming"):
        return {}  # fall back to default

    caller = event["data"]["from_number"]
    # Cheap DB lookup: is this a known VIP?
    customer = lookup_customer(caller)
    if customer and customer.tier == "vip":
        return {
            "prompt":  f"You are a VIP concierge for {customer.name}. Be proactive…",
            "voice":   "john",
            "product": "storm-base",
        }
    return {}  # default agent handles non-VIPs

def lookup_customer(phone: str):
    # ... your CRM integration ...
    pass
```

```javascript Express
import crypto from "node:crypto";
import express from "express";

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

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

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

    const IMPORTANT_TYPES = new Set([
      "telephony.incoming",
      "web.incoming",
    ]);
    if (!IMPORTANT_TYPES.has(event.type)) return res.json({});

    const customer = await lookupCustomer(event.data.from_number);
    if (customer?.tier === "vip") {
      return res.json({
        prompt:  `You are a VIP concierge for ${customer.name}. Be proactive…`,
        voice:   "john",
        product: "storm-base",
      });
    }
    res.json({}); // fall back to default agent
  },
);
```
</CodeGroup>

## 3. 응답 스키마

응답 본문은
[수신 통화 응답 스키마](/ko/webhooks/call-incoming)와
정확히 일치합니다. 자주 사용하는 필드는 다음과 같습니다.

| 필드 | 유형 | 설명 |
|-------|------|-------------|
| `prompt` | 문자열(필수) | 에이전트의 시스템 프롬프트 |
| `voice` | 문자열(필수) | [`GET /v1/voices`](/api-reference/agents#voices)의 음성 ID |
| `product` | 문자열 | 기본값은 `spark`입니다. 실험적 제품은 허용 목록에 등록된 조직에서만 사용할 수 있습니다. |
| `background_track` | 문자열 \| null | 배경 오디오 ID |
| `acknowledgement_prompt_mode` | 문자열 | `auto` 또는 `manual`(확인 기능이 있는 Storm 전용) |
| `acknowledgement_prompt` | 문자열 | 모드가 `manual`인 경우 필수입니다. |
| `tools` | 배열 | 인라인 함수 도구 스키마 — [함수 도구](/ko/tools/overview)를 참조하세요 |

<Note>
  통화별 발화 순서와 `max_hold_seconds`는 웹훅 응답에서 사용할 수 없습니다.
  참조하는 [에이전트](/api-reference/agents)에서 설정하세요.
</Note>

### 저장된 에이전트를 유지하고 변수 제공

조직의 저장된 에이전트를 통화별 데이터와 함께 사용하려면 `{"agent_id": 12, "variables": {"name": "Ada"}}`를 반환합니다.
프롬프트에는 `{{name}}` 또는 `{{name|Friend}}`를 포함할 수 있습니다. 웹훅 변수는 요청 수준 변수보다 우선하여 병합됩니다. null은 자리 표시자의 기본값을 사용하며, 기본값이 없으면 빈 텍스트를 사용합니다. 최종 값과 확인되지 않은 이름은 통화 세부 정보 및 완료 웹훅에 표시됩니다.
저장된 에이전트 응답은 `agent_id`, `variables`, `timezone`만 허용합니다. `prompt`가
있으면 응답은 인라인 구성을 사용하고 `agent_id`를 무시합니다(null 또는 정수가 아닌 메타데이터 포함).
인라인 프롬프트는 여전히 유효해야 합니다. 인라인 구성 응답에는 `variables`도 포함할 수 있습니다. 저장된 에이전트 응답은
전화 및 위젯 통화 모두에서 에이전트에 배포된 A/B 분할을 사용한 후 변수를 렌더링합니다. 제한 사항 및 세션 API 지원은 [통화 변수](/ko/guides/call-variables)를
참조합니다. 차단 구성은 레거시 전화번호/조직 URL 또는 웹훅 모드 위젯 키에서 제공됩니다.
엔드포인트 시스템 수신 이벤트는 알림 전용입니다.

### 통화별 시간대 설정

하나의 에이전트가 서로 다른 시간대의 연락처에 응답할 수 있습니다. 저장된 에이전트 또는 인라인 응답에 IANA 이름인 `timezone`을 추가하면, 에이전트는 이 통화에서 에이전트 자체 시간대보다 해당 시간대의 날짜와 시간을 먼저 안내하고 가능한 시간을 제안합니다.

```json
{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}
```

`variables`에는 타임스탬프 대신 시간대를 전송합니다. 통화 중에도 에이전트의 시계는 계속 흐르므로 전달한 시간은 오래되어 정확하지 않게 됩니다. IANA 시간대가 아닌 값은 무시되며, 통화는 에이전트의 시간대로 연결됩니다.

## 패턴

### 로그인한 사용자 컨텍스트

웹훅 모드 위젯에서는 방문자의 페이지가 이미 방문자가 누구인지 알고 있습니다.
위젯 SDK가 전달하는 쿼리 문자열 매개변수(`?customer_id=123`)와 함께 웹훅을 호출하고,
서버 측에서 고객을 조회하세요.

### A/B 프롬프트 출시

직접 구현하기 전에 ThunderPhone에는 변형을 정의하고, 트래픽을 분할하며,
변형별 결과를 비교하는 기본 [실험](/ko/guides/concepts) 기능이 있다는 점을 참고하세요.
(`/dashboard/experiments` 및 에이전트 빌더의 **A/B** 탭) 웹훅은 필요하지 않습니다.

그래도 웹훅 측 제어가 필요하다면 `call_id`를 해시하여 버킷을 만드세요.
`0..49`에는 프롬프트 A를 제공하고 `50..99`에는 프롬프트 B를 제공합니다.
선택한 버킷을 자체 DB에 기록하고, 나중에 완료된 통화의 등급과 연관 지으세요.

### 시간 기반 라우팅

영업시간에는 "실시간 지원" 에이전트를 사용하고, 영업시간 이후에는 "메시지 접수"
에이전트를 사용합니다. 핸들러에서 `new Date().getUTCHours()`를 기준으로 간단히 전환합니다.

---

## 다음 단계

<CardGroup cols={2}>
  <Card title="수신 통화 웹훅 참조" icon="phone" href="/ko/webhooks/call-incoming">
    모든 구성 키를 포함한 정확한 요청 및 응답 스키마입니다.
  </Card>
  <Card title="웹훅 서명 검증" icon="shield-check" href="/ko/guides/verify-webhook-signatures">
    HMAC를 한 번 올바르게 설정하고 어디서나 재사용하세요.
  </Card>
  <Card title="도구 통합 구축" icon="screwdriver-wrench" href="/ko/guides/build-tool-integration">
    동적 라우팅과 에이전트별 도구를 결합하세요.
  </Card>
  <Card title="전송 의미론" icon="bolt" href="/ko/webhooks/overview">
    재시도, 순서 지정, 시간 초과입니다.
  </Card>
</CardGroup>
