> ## 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.

# Dynamische Konfiguration pro Anruf

> Wählen Sie für jeden eingehenden Anruf anhand benutzerdefinierter Logik in einem Webhook einen Agenten aus oder schreiben Sie einen Prompt um.

Standardmäßig ist jeder Telefonnummer und jedem veröffentlichbaren Schlüssel ein statischer Agent zugewiesen. Wenn Sie **pro Anrufer** oder **pro Besucher** Anpassungen benötigen — VIP-Routing, Kontext angemeldeter Benutzer, A/B-Tests für Prompts — wechseln Sie in den Webhook-Modus und lassen Sie Ihren Server entscheiden.

## So funktioniert es

1. Abonnieren Sie das Ereignis [`telephony.incoming`](/de/webhooks/events)
   (Telefon) oder [`web.incoming`](/de/webhooks/events) (Widget).
   Beide sind **blockierende** Webhooks: ThunderPhone wartet bis zu
   10 Sekunden auf Ihre Antwort, bevor der Anruf fortgesetzt wird.
2. ThunderPhone sendet Ihnen `{call_id, from_number, to_number}` (Widget-
   Sitzungen enthalten statt Nummern widgetspezifische Felder — siehe das
   [Anfrageschema](/de/webhooks/call-incoming)).
3. Ihr Server antwortet mit einer Agentenkonfiguration (Prompt, Stimme,
   Produkt, Tools). ThunderPhone verwendet diese Konfiguration für den Anruf.
4. Wenn Sie `{}` zurückgeben, ein Timeout auftritt oder ein Fehler entsteht,
   wird der statisch zugewiesene Agent als Fallback verwendet. Sichere
   Standardeinstellung.

<Note>
  Funktioniert identisch für Telefonanrufe (`telephony.incoming`) und Widget-
  Sitzungen (`web.incoming`), unabhängig davon, ob sie an einen Webhook-Endpunkt
  oder an den Legacy-Webhook mit einer einzelnen URL zugestellt werden.
</Note>

## 1. Webhook-Ziel konfigurieren

<Tabs>
  <Tab title="Telefonanrufe">
    Abonnieren Sie für Telefonnummern `telephony.incoming` für Ihren Endpunkt:

    ```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"]
      }'
    ```

    Die Antwort enthält ein einmaliges `secret` — speichern Sie es; Sie benötigen
    es für die Signaturverifizierung.
  </Tab>

  <Tab title="Web-Widget">
    Erstellen Sie für Widget-Sitzungen einen veröffentlichbaren Schlüssel im
    `mode="webhook"` mit Ihrer Endpunkt-URL als fest hinterlegtem Wert:

    ```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"]
      }'
    ```

    Das Widget sendet bei jedem Sitzungsstart eine POST-Anfrage an diese URL.
  </Tab>
</Tabs>

## 2. Implementieren Sie den Handler

Drei Faustregeln:

* **Überprüfen Sie die Signatur** bei jeder Anfrage (siehe
  [Webhook-Signaturen überprüfen](/de/guides/verify-webhook-signatures)).
  Überspringen Sie dies nicht in der Entwicklung — machen Sie es einmal richtig und verwenden Sie es erneut.
* **Antworten Sie schnell**. Zehn Sekunden sind die harte Obergrenze, und jede Sekunde ist
  Stille für den Anrufer. Führen Sie bei Bedarf Datenbankabfragen durch, aber
  rufen Sie nachgelagerte LLMs nicht synchron auf — wenn Sie eine dynamische
  Prompt-Generierung benötigen, berechnen Sie diese vorab und speichern Sie sie im Cache.
* **Sorgen Sie für einen sauberen Fallback**. Jeder unerwartete Zustand sollte `{}` zurückgeben, damit
  der statisch zugewiesene Agent den Anruf bearbeitet.

<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. Antwortschema

Der Antworttext entspricht exakt dem
[Antwortschema für eingehende Anrufe](/de/webhooks/call-incoming).
Die häufig verwendeten Felder:

| Feld                          | Typ                   | Beschreibung                                                                     |
| ----------------------------- | --------------------- | -------------------------------------------------------------------------------- |
| `prompt`                      | string (erforderlich) | System-Prompt für den Agenten                                                    |
| `voice`                       | string (erforderlich) | Sprach-ID aus [`GET /v1/voices`](/api-reference/agents#voices)                   |
| `product`                     | string                | Standard ist `spark`                                                             |
| `background_track`            | string \| null        | ID für Hintergrund-Audio                                                         |
| `acknowledgement_prompt_mode` | string                | `auto` oder `manual` (nur Storm-with-ack)                                        |
| `acknowledgement_prompt`      | string                | Erforderlich, wenn der Modus `manual` ist                                        |
| `tools`                       | array                 | Inline-Schemas für Funktions-Tools — siehe [Funktions-Tools](/de/tools/overview) |

<Note>
  Die Sprechreihenfolge pro Anruf und `max_hold_seconds` sind in der
  Webhook-Antwort nicht verfügbar. Legen Sie sie für den
  referenzierten [Agenten](/api-reference/agents) fest.
</Note>

## Muster

### Kontext angemeldeter Benutzer

In Widgets im Webhook-Modus weiß die Seite des Besuchers bereits, wer er
ist. Rufen Sie Ihren Webhook mit einem Query-String-Parameter auf, den das
Widget-SDK weiterleitet (`?customer_id=123`), und suchen Sie den Kunden
serverseitig.

### A/B-Prompt-Rollout

Bevor Sie dies selbst implementieren, beachten Sie, dass ThunderPhone über eine native
Funktion für [Experimente](/de/guides/concepts) verfügt
(`/dashboard/experiments` und den Tab **A/B** im Agent-Builder), die
Varianten definiert, Traffic aufteilt und Ergebnisse pro Variante vergleicht —
kein Webhook erforderlich.

Wenn Sie dennoch eine Steuerung auf Webhook-Seite benötigen: Hash von `call_id` → Bucket;
stellen Sie Prompt A für `0..49` und Prompt B für `50..99` bereit. Speichern Sie den
gewählten Bucket in Ihrer eigenen Datenbank und korrelieren Sie ihn später mit der
Bewertung des abgeschlossenen Anrufs.

### Zeitbasierte Weiterleitung

Geschäftszeiten → Agent für „Live-Support“; außerhalb der Geschäftszeiten → Agent zum
„Aufnehmen einer Nachricht“. Reine Umschaltung auf `new Date().getUTCHours()` in Ihrem Handler.

***

## Nächste Schritte

<CardGroup cols={2}>
  <Card title="Referenz für Webhooks bei eingehenden Anrufen" icon="phone" href="/de/webhooks/call-incoming">
    Exakte Anfrage- und Antwortschemata, einschließlich aller Konfigurationsschlüssel.
  </Card>

  <Card title="Webhook-Signaturen verifizieren" icon="shield-check" href="/de/guides/verify-webhook-signatures">
    Richten Sie HMAC einmal korrekt ein und verwenden Sie es überall wieder.
  </Card>

  <Card title="Eine Tool-Integration erstellen" icon="screwdriver-wrench" href="/de/guides/build-tool-integration">
    Kombinieren Sie dynamische Weiterleitung mit agentenspezifischen Tools.
  </Card>

  <Card title="Zustellungssemantik" icon="bolt" href="/de/webhooks/overview">
    Wiederholungen, Reihenfolge, Timeouts.
  </Card>
</CardGroup>
