---
title: "Konfigurasi dinamis per panggilan"
description: "Pilih agen yang menjawab — atau tulis ulang Prompt dan pengaturannya — secara terpisah untuk setiap panggilan masuk, berdasarkan logika khusus dalam webhook yang Anda kendalikan."
---

Secara default, setiap nomor telepon dan kunci yang dapat dipublikasikan memiliki agen statis yang ditetapkan. Saat Anda memerlukan penyesuaian **per-penelepon** atau **per-pengunjung** — perutean VIP, konteks pengguna yang masuk, pengujian Prompt A/B — beralihlah ke mode webhook dan biarkan server Anda yang menentukan.

## Cara kerjanya

1. Anda berlangganan event [`telephony.incoming`](/id/webhooks/events)
   (telepon) atau [`web.incoming`](/id/webhooks/events) (widget).
   Keduanya adalah webhook **yang memblokir**: ThunderPhone menunggu
   respons Anda sebelum melanjutkan panggilan, hingga 13 detik untuk panggilan
   telepon (termasuk satu kali percobaan ulang) dan 10 detik untuk sesi widget.
2. ThunderPhone mengirimkan `{call_id, from_number, to_number}` kepada Anda (sesi
   widget membawa kolom khusus widget, bukan nomor — lihat
   [skema permintaan](/id/webhooks/call-incoming)).
3. Server Anda merespons dengan konfigurasi agen (Prompt, suara,
   produk, alat). ThunderPhone menggunakan konfigurasi tersebut untuk panggilan.
4. Tidak ada agen cadangan. Jika Anda mengembalikan `{}` atau konfigurasi
   yang tidak valid, atau permintaan tetap gagal setelah
   [percobaan ulang](/id/webhooks/call-incoming), panggilan
   telepon langsung diakhiri dan dicatat dengan `end_reason`
   `inbound_config_webhook_failed`; permintaan sesi widget gagal.

<Note>
  Berfungsi secara identik untuk panggilan telepon (`telephony.incoming`) dan sesi
  widget (`web.incoming`), baik dikirimkan ke endpoint webhook
  maupun ke webhook URL tunggal lama.
</Note>

<Warning>
  **Tidak ada pengumuman persetujuan ThunderPhone yang diputar pada panggilan
  yang dikonfigurasi webhook inline.** Jalur ini melewati pengumuman awal panggilan
  tingkat agen dan secara tegas dikecualikan dari kerangka pengumuman
  persetujuan ThunderPhone (Syarat Layanan, bagian "Perekaman dan persetujuan").
  Organisasi Anda bertanggung jawab penuh atas semua pemberitahuan dan persetujuan
  perekaman, pemantauan, partisipasi AI, dan identifikasi penelepon pada panggilan ini.
  Sediakan pemberitahuan dan persetujuan tersebut dalam alur Anda sendiri — misalnya
  dalam skrip pembuka Prompt — sebelum mengaktifkan mode webhook inline. Respons webhook
  yang merujuk `agent_id` tersimpan menggunakan kebijakan perekaman dan pengungkapan
  normal agen tersebut.
</Warning>

## 1. Konfigurasikan tujuan webhook

<Tabs>
<Tab title="Panggilan telepon">
Untuk nomor telepon, berlangganankan endpoint Anda ke `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"]
  }'
```

Responsnya menyertakan `secret` sekali pakai — simpan; Anda akan menggunakannya
untuk verifikasi tanda tangan.
</Tab>
<Tab title="Widget web">
Untuk sesi widget, buat kunci yang dapat dipublikasikan dalam `mode="webhook"`
dengan URL endpoint Anda tertanam:

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

Widget akan mengirim POST ke URL ini pada setiap awal sesi.
</Tab>
</Tabs>

## 2. Implementasikan handler

Tiga aturan praktis:

- **Verifikasi tanda tangan** pada setiap permintaan (lihat
  [Verifikasi tanda tangan webhook](/id/guides/verify-webhook-signatures)).
  Jangan lewati ini saat pengembangan — lakukan dengan benar sekali lalu gunakan kembali.
- **Respons dengan cepat**. Upaya pertama panggilan telepon diputus pada 10
  detik dan seluruh pertukaran pada 13 detik, dan setiap detik adalah keheningan
  bagi penelepon. Lakukan pencarian database jika diperlukan, tetapi jangan panggil
  LLM hilir secara sinkron — jika Anda ingin menghasilkan prompt secara dinamis,
  prahitung dan simpan dalam cache.
- **Lakukan fallback dengan rapi**. Untuk keadaan tak terduga apa pun, berikan
  `prompt` dan `voice` default (atau `agent_id` yang tersimpan). Mengembalikan `{}`
  atau status error akan mengakhiri panggilan.

<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. Skema respons

Isi respons sama persis dengan
[skema respons panggilan masuk](/id/webhooks/call-incoming).
Kolom yang umum digunakan:

| Bidang | Tipe | Deskripsi |
|-------|------|-------------|
| `prompt` | string (wajib) | Prompt sistem untuk agen |
| `voice` | string (wajib) | ID suara dari [`GET /v1/voices`](/api-reference/agents#voices) |
| `product` | string | Defaultnya adalah `spark`. Produk eksperimental hanya tersedia untuk organisasi dalam daftar izin. |
| `background_track` | string \| null | ID audio ambient |
| `acknowledgement_prompt_mode` | string | `auto` atau `manual` (khusus Storm dengan ack) |
| `acknowledgement_prompt` | string | Wajib saat mode adalah `manual` |
| `language_switching` | string | Untuk agen multibahasa, `ask_first` atau `follow_caller` |
| `tools` | array | Skema alat fungsi inline — lihat [Alat Fungsi](/id/tools/overview) |

<Note>
  Urutan bicara per panggilan dan `max_hold_seconds` tidak tersedia pada
  respons webhook. Atur keduanya pada
  [Agen](/api-reference/agents) yang Anda referensikan.
</Note>

### Pertahankan agen tersimpan dan berikan variabel

Kembalikan `{"agent_id": 12, "variables": {"name": "Ada"}}` untuk menggunakan agen tersimpan organisasi tersebut
dengan data per panggilan. Prompt-nya dapat berisi `{{name}}` atau
`{{name|Friend}}`. Variabel webhook digabungkan di atas variabel tingkat permintaan; null
menggunakan nilai default placeholder, atau teks kosong jika tidak ada yang disediakan. Nilai akhir
dan nama yang belum terselesaikan muncul dalam detail panggilan dan webhook penyelesaian.
Respons agen tersimpan hanya menerima `agent_id`, `variables`, dan `timezone`. Jika `prompt`
ada, respons menggunakan konfigurasi inline dan mengabaikan `agent_id` (termasuk
metadata null atau bukan bilangan bulat); Prompt inline tetap harus valid. Respons konfigurasi
inline juga dapat menyertakan `variables`. Respons agen tersimpan menggunakan pembagian A/B agen
yang diterapkan pada panggilan telepon dan widget, lalu merender variabel. Lihat [variabel panggilan](/id/guides/call-variables)
untuk batas dan dukungan API sesi. Konfigurasi pemblokiran berasal dari
URL nomor telepon/organisasi lama atau kunci widget mode webhook; event masuk sistem endpoint
hanya berupa notifikasi.

### Tetapkan zona waktu per panggilan

Satu agen dapat menjawab kontak di zona waktu yang berbeda. Tambahkan `timezone`,
nama IANA, ke respons agen tersimpan atau inline dan agen akan menyebutkan tanggal
dan waktu serta menawarkan waktu dalam zona tersebut untuk panggilan ini, mendahului
zona waktu agen sendiri:

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

Kirimkan zona, bukan stempel waktu, dalam `variables`: jam agen terus berjalan
selama panggilan, dan waktu yang Anda kirim akan kedaluwarsa. Nilai yang bukan
zona IANA akan diabaikan dan panggilan terhubung dalam zona agen.

## Pola

### Konteks pengguna yang sudah Masuk

Dalam widget mode webhook, halaman pengunjung sudah mengetahui identitas
mereka. Panggil webhook Anda dengan parameter query string yang diteruskan oleh SDK
widget (`?customer_id=123`) dan cari data pelanggan di sisi server.

### Peluncuran Prompt A/B

Sebelum membuatnya sendiri, ketahui bahwa ThunderPhone memiliki fitur asli
[Experiments](/id/guides/concepts)
(`/dashboard/experiments` dan tab **A/B** di pembuat agen) yang
menentukan varian, membagi trafik, dan membandingkan hasil per varian —
tanpa webhook.

Jika Anda tetap memerlukan kontrol di sisi webhook: hash `call_id` → bucket;
sajikan prompt A untuk `0..49` dan prompt B untuk `50..99`. Catat bucket yang
Anda pilih dalam DB Anda sendiri dan kemudian korelasikan dengan nilai panggilan
yang telah selesai.

### Perutean berbasis waktu

Jam kerja → agen "dukungan langsung"; di luar jam kerja → agen "mencatat pesan".
Buat percabangan sederhana berdasarkan `new Date().getUTCHours()` dalam handler Anda.

---

## Langkah berikutnya

<CardGroup cols={2}>
  <Card title="Referensi webhook panggilan masuk" icon="phone" href="/id/webhooks/call-incoming">
    Skema permintaan + respons lengkap, termasuk setiap kunci konfigurasi.
  </Card>
  <Card title="Verifikasi tanda tangan webhook" icon="shield-check" href="/id/guides/verify-webhook-signatures">
    Buat HMAC dengan benar sekali; gunakan kembali di mana saja.
  </Card>
  <Card title="Buat integrasi tool" icon="screwdriver-wrench" href="/id/guides/build-tool-integration">
    Gabungkan perutean dinamis dengan tool per agen.
  </Card>
  <Card title="Semantik pengiriman" icon="bolt" href="/id/webhooks/overview">
    Percobaan ulang, urutan, waktu habis.
  </Card>
</CardGroup>
