---
title: "Dynamische configuratie per oproep"
description: "Kies voor elke inkomende oproep afzonderlijk de spraakagent die antwoordt, of herschrijf de prompt en instellingen ervan, op basis van aangepaste logica in een webhook die je zelf beheert."
---

Standaard heeft elk telefoonnummer en elke publiceerbare sleutel een vaste
spraakagent toegewezen. Wanneer je aanpassing **per beller** of **per bezoeker** nodig hebt
— VIP-routering, context van ingelogde gebruikers, A/B-tests van prompts — schakel je over naar
webhookmodus en laat je server beslissen.

## Zo werkt het

1. Je abonneert je op de gebeurtenis [`telephony.incoming`](/nl/webhooks/events)
   (telefoon) of [`web.incoming`](/nl/webhooks/events) (widget).
   Beide zijn **blokkerende** webhooks: ThunderPhone wacht op je
   reactie voordat de oproep wordt voortgezet, maximaal 13 seconden voor een
   telefoongesprek (inclusief één nieuwe poging) en 10 seconden voor een widgetsessie.
2. ThunderPhone stuurt je `{call_id, from_number, to_number}` (widget-
   sessies bevatten widgetspecifieke velden in plaats van nummers — zie het
   [aanvraagschema](/nl/webhooks/call-incoming)).
3. Je server reageert met een spraakagentconfiguratie (prompt, stem,
   product, tools). ThunderPhone gebruikt die configuratie voor de oproep.
4. Er is geen terugvalspraakagent. Als je `{}` of een ongeldige
   configuratie retourneert, of als het verzoek na de
   [nieuwe poging](/nl/webhooks/call-incoming) nog steeds mislukt, wordt het
   telefoongesprek onmiddellijk beëindigd en geregistreerd met `end_reason`
   `inbound_config_webhook_failed`; een widgetsessieverzoek mislukt.

<Note>
  Werkt identiek voor telefoongesprekken (`telephony.incoming`) en widgetsessies
  (`web.incoming`), ongeacht of ze worden afgeleverd bij een webhookeindpunt
  of bij de verouderde webhook met één URL.
</Note>

<Warning>
  **Er wordt geen ThunderPhone-toestemmingsmelding afgespeeld bij oproepen
  die inline via een webhook zijn geconfigureerd.** Dit pad omzeilt de
  melding bij het begin van de oproep op agentniveau en valt nadrukkelijk buiten
  het kader voor toestemmingsmeldingen van ThunderPhone (Servicevoorwaarden,
  sectie "Opname en toestemming"). Jouw organisatie is verantwoordelijk voor alle
  meldingen en toestemmingen voor opname, monitoring, AI-deelname en
  belleridentificatie bij deze oproepen. Neem deze op in je eigen flow — bijvoorbeeld
  in het openingsscript van de prompt — voordat je de inline webhookmodus inschakelt.
  Een webhookreactie die verwijst naar een opgeslagen `agent_id` gebruikt het normale
  beleid voor opnames en meldingen van die spraakagent.
</Warning>

## 1. Configureer de webhookbestemming

<Tabs>
<Tab title="Telefoongesprekken">
Voor telefoonnummers abonneer je je eindpunt op `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"]
  }'
```

Het antwoord bevat een eenmalige `secret` — sla deze op; je gebruikt deze
voor handtekeningverificatie.
</Tab>
<Tab title="Webwidget">
Voor widgetsessies maak je een publiceerbare sleutel in `mode="webhook"`
waarbij de URL van je eindpunt is ingebouwd:

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

De widget voert bij elke sessiestart een POST uit naar deze URL.
</Tab>
</Tabs>

## 2. Implementeer de handler

Drie vuistregels:

- **Verifieer de handtekening** bij elk verzoek (zie
  [Webhookhandtekeningen verifiëren](/nl/guides/verify-webhook-signatures)).
  Sla dit niet over in development — doe het één keer goed en hergebruik het.
- **Reageer snel**. De eerste poging van een telefoongesprek wordt na 10
  seconden afgebroken en de hele uitwisseling na 13, en elke seconde is stilte
  voor de beller. Voer indien nodig databaseopzoekingen uit, maar roep
  downstream-LLM's niet synchroon aan — als je dynamische promptgeneratie wilt,
  bereken deze dan vooraf en sla deze in de cache op.
- **Val netjes terug**. Geef voor elke onverwachte status een standaardwaarde
  voor `prompt` en `voice` terug (of een opgeslagen `agent_id`). Het retourneren van `{}`
  of een foutstatus beëindigt het gesprek.

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

De responsbody komt exact overeen met het
[antwoordschema voor inkomende oproepen](/nl/webhooks/call-incoming).
De meestgebruikte velden:

| Veld | Type | Beschrijving |
|-------|------|-------------|
| `prompt` | tekenreeks (vereist) | Systeemprompt voor de agent |
| `voice` | tekenreeks (vereist) | Spraak-ID van [`GET /v1/voices`](/api-reference/agents#voices) |
| `product` | tekenreeks | Standaard ingesteld op `spark`. Experimentele producten zijn alleen beschikbaar voor organisaties op de toelatingslijst. |
| `background_track` | tekenreeks \| null | ID van omgevingsaudio |
| `acknowledgement_prompt_mode` | tekenreeks | `auto` of `manual` (alleen Storm-met-bevestiging) |
| `acknowledgement_prompt` | tekenreeks | Vereist wanneer de modus `manual` is |
| `tools` | array | Inline schema's voor functietools — zie [Functietools](/nl/tools/overview) |

<Note>
  Spreekvolgorde per oproep en `max_hold_seconds` zijn niet beschikbaar in de
  webhookrespons. Stel ze in op de
  [Agent](/api-reference/agents) waarnaar je verwijst.
</Note>

### Behoud een opgeslagen agent en geef variabelen door

Retourneer `{"agent_id": 12, "variables": {"name": "Ada"}}` om de opgeslagen agent van die organisatie
met gegevens per oproep te gebruiken. De prompt kan `{{name}}` of
`{{name|Friend}}` bevatten. Webhookvariabelen worden samengevoegd met variabelen op verzoekniveau; null
gebruikt de standaardwaarde van de placeholder, of lege tekst als er geen is opgegeven. De uiteindelijke
waarden en niet-opgeloste namen verschijnen in oproepdetails en webhooks bij voltooiing.
Reacties van opgeslagen agents accepteren alleen `agent_id`, `variables` en `timezone`. Als `prompt`
aanwezig is, gebruikt de reactie inlineconfiguratie en wordt `agent_id` genegeerd (inclusief
null- of niet-gehele metagegevens); de inlineprompt moet nog steeds geldig zijn. Reacties met inline-
configuratie kunnen ook `variables` bevatten. Reacties van opgeslagen agents gebruiken de uitgerolde
A/B-verdeling van de agent voor zowel telefoon- als widgetoproepen en vullen vervolgens variabelen in. Zie [oproepvariabelen](/nl/guides/call-variables)
voor limieten en ondersteuning voor de sessie-API. Blokkerende configuratie komt van de verouderde
URL voor telefoonnummer/organisatie of een widgetsleutel in webhookmodus; binnenkomende gebeurtenissen van het
endpoint-systeem zijn uitsluitend meldingen.

### Stel de tijdzone per gesprek in

Eén spraakagent kan oproepen beantwoorden voor contacten in verschillende tijdzones. Voeg `timezone`, een
IANA-naam, toe aan een opgeslagen agent of inline respons. De agent vermeldt dan de datum
en tijd en biedt voor dit gesprek tijden aan in die zone, vóór de eigen tijdzone van de agent:

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

Stuur de zone in plaats van een tijdstempel in `variables`: de klok van de agent loopt
door tijdens het gesprek en een tijd die je doorgeeft, veroudert. Een waarde die geen
IANA-zone is, wordt genegeerd en het gesprek wordt verbonden in de zone van de agent.

## Patronen

### Context van ingelogde gebruiker

In widgets in webhookmodus weet de pagina van de bezoeker al wie deze
is. Roep je webhook aan met een querystringparameter die de widget-SDK
doorstuurt (`?customer_id=123`) en zoek de klant server-side op.

### A/B-uitrol van prompts

Voordat je dit zelf implementeert, moet je weten dat ThunderPhone een native
functie voor [Experiments](/nl/guides/concepts) heeft
(`/dashboard/experiments` en het tabblad **A/B** van de agentbuilder) die
varianten definieert, verkeer verdeelt en resultaten per variant vergelijkt —
geen webhook vereist.

Als je toch controle aan webhookzijde nodig hebt: hash `call_id` → bucket;
gebruik prompt A voor `0..49` en prompt B voor `50..99`. Leg vast welke
bucket je hebt gekozen in je eigen database en koppel die later aan de
beoordeling van de voltooide oproep.

### Routering op basis van tijd

Tijdens kantooruren → agent voor "live ondersteuning"; buiten kantooruren → agent
voor "een bericht opnemen". Een eenvoudige switch op `new Date().getUTCHours()`
in je handler.

---

## Volgende stappen

<CardGroup cols={2}>
  <Card title="Referentie voor webhook voor inkomende oproepen" icon="phone" href="/nl/webhooks/call-incoming">
    Exacte aanvraag- en antwoordschema's, inclusief elke configuratiesleutel.
  </Card>
  <Card title="Webhookhandtekeningen verifiëren" icon="shield-check" href="/nl/guides/verify-webhook-signatures">
    Stel de HMAC één keer correct in; hergebruik hem overal.
  </Card>
  <Card title="Een toolintegratie bouwen" icon="screwdriver-wrench" href="/nl/guides/build-tool-integration">
    Combineer dynamische routering met tools per agent.
  </Card>
  <Card title="Afleversemantiek" icon="bolt" href="/nl/webhooks/overview">
    Nieuwe pogingen, volgorde, time-outs.
  </Card>
</CardGroup>
