> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 通話ごとの動的設定

> Webhook内のカスタムロジックに基づき、着信通話ごとにエージェントを選択するか、プロンプトを書き換えます。

デフォルトでは、すべての電話番号と公開可能キーに固定エージェントが
割り当てられています。**発信者ごと**または**訪問者ごと**のカスタマイズ
（VIP ルーティング、ログイン済みユーザーのコンテキスト、A/B プロンプトテスト）が必要な場合は、
Webhook モードに切り替え、サーバー側で決定します。

## 仕組み

1. [`telephony.incoming`](/ja/webhooks/events)
   （電話）または [`web.incoming`](/ja/webhooks/events)（ウィジェット）
   イベントを購読します。どちらも**ブロッキング**Webhook です。ThunderPhone は
   通話を続行する前に、応答を最大 10 秒間待機します。
2. ThunderPhone から `{call_id, from_number, to_number}` が送信されます（ウィジェット
   セッションでは番号の代わりにウィジェット固有のフィールドが含まれます。詳細は
   [リクエストスキーマ](/ja/webhooks/call-incoming)を参照してください）。
3. サーバーはエージェント設定（プロンプト、音声、プロダクト、ツール）を返します。
   ThunderPhone はその設定を通話に使用します。
4. `{}` を返した場合、タイムアウトした場合、またはエラーが発生した場合は、固定で割り当てられた
   エージェントがフォールバックとして使用されます。安全なデフォルトです。

<Note>
  Webhook エンドポイントに配信する場合も、レガシーの単一 URL Webhook に配信する場合も、
  電話（`telephony.incoming`）とウィジェットセッション（`web.incoming`）で同じように動作します。
</Note>

## 1. Webhook の送信先を設定する

<Tabs>
  <Tab title="電話">
    電話番号では、エンドポイントを `telephony.incoming` に購読させます。

    ```bash theme={null}
    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="Web ウィジェット">
    ウィジェットセッションでは、エンドポイント URL を埋め込んだ `mode="webhook"` の公開可能キーを作成します。

    ```bash theme={null}
    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. ハンドラーを実装する

3つの基本ルール:

* すべてのリクエストで**署名を検証**します（[Webhook署名を検証する](/ja/guides/verify-webhook-signatures)を参照）。
  開発環境でも省略せず、一度正しく実装して再利用してください。
* **迅速に応答**します。上限は10秒であり、1秒ごとに発信者には無音が続きます。必要に応じてデータベース検索は行えますが、ダウンストリームのLLMを同期的に呼び出さないでください。動的なプロンプト生成が必要な場合は、事前に計算してキャッシュします。
* **適切にフォールバック**します。想定外の状態では `{}` を返し、静的に割り当てられたエージェントが通話を処理できるようにします。

<CodeGroup>
  ```python FastAPI theme={null}
  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 theme={null}
  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. レスポンススキーマ

レスポンス本文は[着信通話レスポンススキーマ](/ja/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`                       | 配列          | インライン関数ツールスキーマ — [関数ツール](/ja/tools/overview)を参照        |

<Note>
  通話ごとの発話順序と `max_hold_seconds` はWebhookレスポンスでは利用できません。参照する[エージェント](/api-reference/agents)で設定してください。
</Note>

## パターン

### ログイン済みユーザーのコンテキスト

webhook モードのウィジェットでは、訪問者が誰であるかをページがすでに把握しています。
ウィジェット SDK が転送するクエリ文字列パラメータ（`?customer_id=123`）を付けて webhook を呼び出し、サーバー側で顧客を検索します。

### A/B プロンプトのロールアウト

これを独自実装する前に、ThunderPhone にはバリアントの定義、トラフィックの分割、バリアントごとの結果比較を行うネイティブの
[実験](/ja/guides/concepts)機能
（`/dashboard/experiments` とエージェントビルダーの **A/B** タブ）があることに留意してください。
webhook は不要です。

それでも webhook 側で制御する必要がある場合: `call_id` をハッシュ化してバケットに割り当てます。
`0..49` にはプロンプト A、`50..99` にはプロンプト B を提供します。選択したバケットを独自の DB に記録し、後で完了した通話の評価と関連付けます。

### 時間ベースのルーティング

営業時間内 → 「ライブサポート」エージェント、営業時間外 → 「メッセージ受付」エージェント。ハンドラー内で `new Date().getUTCHours()` を使用するだけの単純な切り替えです。

***

## 次のステップ

<CardGroup cols={2}>
  <Card title="着信通話 webhook リファレンス" icon="phone" href="/ja/webhooks/call-incoming">
    すべての設定キーを含む、正確なリクエストおよびレスポンスのスキーマ。
  </Card>

  <Card title="webhook 署名を検証" icon="shield-check" href="/ja/guides/verify-webhook-signatures">
    HMAC を一度正しく設定し、どこでも再利用します。
  </Card>

  <Card title="ツール統合を構築" icon="screwdriver-wrench" href="/ja/guides/build-tool-integration">
    動的ルーティングとエージェントごとのツールを組み合わせます。
  </Card>

  <Card title="配信セマンティクス" icon="bolt" href="/ja/webhooks/overview">
    再試行、順序、タイムアウト。
  </Card>
</CardGroup>
