telephony.incoming / web.incoming
Webhook bloqueante que define a configuração de uma chamada recebida em tempo real.
Quando uma chamada telefônica recebida chega a um número sem um
agente atribuído, ou uma sessão de widget web é iniciada com uma chave publicável em
mode="webhook", o ThunderPhone envia uma solicitação bloqueante
telephony.incoming / web.incoming para sua
URL de webhook legada
e aguarda até 10 segundos por uma resposta de configuração. Use essa
troca para escolher dinamicamente um prompt, uma voz e ferramentas por chamada —
consulte o guia de configuração dinâmica de chamadas
para conhecer o padrão completo.
A troca bloqueante não tem alternativa: se seu handler retornar um
status diferente de 2xx, exceder o tempo limite ou retornar uma configuração
que não passe na validação, a chamada será rejeitada (a chamada telefônica
não será conectada; a solicitação da sessão do widget falhará com 502/422).
Responda rapidamente — quem liga ouve o tom de chamada enquanto você decide.
Payload da solicitação
Para chamadas telefônicas (telephony.incoming):
{
"type": "telephony.incoming",
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
}
}| Campo | Tipo | Descrição |
|---|---|---|
call_id | inteiro | ID da chamada — estável em todos os eventos desta chamada |
from_number | string | Número E.164 de quem liga |
to_number | string | Destino E.164 (um dos seus números ThunderPhone) |
Para sessões de widget web (web.incoming), o data identifica a
página de incorporação em vez de números de telefone:
{
"type": "web.incoming",
"data": {
"call_id": 987654322,
"origin_domain": "https://example.com",
"publishable_key_prefix": "pk_live_a1b2"
}
}| Campo | Tipo | Descrição |
|---|---|---|
call_id | inteiro | ID da chamada |
origin_domain | string | A origem da página que hospeda o widget |
publishable_key_prefix | string | Primeiros caracteres da chave publicável que abriu a sessão |
language, primary_language | string | Presente quando a sessão do widget solicitou uma substituição de idioma |
voice | string | Presente quando a sessão do widget solicitou uma substituição de voz |
website_context | string | Presente quando o widget transmitiu contexto de página por sessão |
Esquema de resposta
Retorne um objeto JSON que descreva a configuração do agente para esta chamada.
prompt e voice são obrigatórios; todos os demais campos são opcionais.
{
"prompt": "You are a helpful booking assistant for Acme Restaurant.",
"voice": "john",
"product": "spark",
"background_track": null,
"tools": []
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
prompt | string | sim | Prompt de sistema que orienta o agente |
voice | string | sim | ID de voz de GET /v1/voices, por exemplo, john. voice_name é aceito como alias. Vozes desconhecidas falham na validação e rejeitam a chamada |
product | string | não | O padrão é spark. Permitidos: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack |
thinking_level | string | não | minimal, base (padrão) ou extra. Substituído para produtos Storm: storm-extra* força extra, outros storm-* forçam base |
audio_context_mode | string | não | full (padrão) ou reduced |
watchdog_enabled | boolean | não | Ativa a supervisão para esta chamada. O padrão é false |
additional_audio_context | boolean | null | não | Inclui as últimas interações do áudio de quem liga em vez de apenas a interação mais recente, melhorando correções e a coleta de dados com muita soletração/números, com um pequeno impacto na latência/custo. É ativado por padrão para sessões recebidas e desativado para chamadas telefônicas de saída; null mantém o padrão |
storm_feedback_mode | string | não | none, acknowledgement (padrão) ou tick |
language | string | não | Abreviação para primary_language |
primary_language | string | não | Código de idioma, normalizado (padrão en). Códigos que não podem ser resolvidos rejeitam a chamada |
has_additional_languages | boolean | não | O padrão é false |
additional_languages | array of string | não | Idiomas adicionais para os quais o agente pode alternar |
native_voice_switching | boolean | não | O padrão é false. Quando a chamada alterna para outro idioma, troca para uma voz nativa desse idioma (correspondente por gênero) em vez de manter a voz configurada |
background_track | string | null | não | ID de áudio ambiente ou null |
acknowledgement_prompt_mode | string | não | auto (padrão) ou manual (produtos Storm com confirmação) |
acknowledgement_prompt | string | não | Usado quando acknowledgement_prompt_mode="manual" |
silence_interval_seconds | integer | null | não | 5–120. Segundos de silêncio de quem liga antes de uma verificação |
silence_max_checkins | integer | null | não | 1–10 |
silence_checkins_enabled | boolean | não | O padrão é true |
connect_tone_enabled | boolean | não | O padrão é false |
voicemail_action | string | não | prompt (padrão), hangup ou message |
voicemail_message | string | não | Usado quando voicemail_action="message" |
agent_name | string | não | Nome de exibição informado aos painéis e ao widget |
org_name | string | não | Nome de exibição da organização para a persona do agente |
tools | array | não | Esquemas inline de ferramentas de função (consulte Ferramentas de função) |
call_id | integer | não | Eco opcional do ID da chamada da solicitação; ignorado |
Como prompt e voice são obrigatórios, retornar {} ou qualquer
resposta que falhe na validação rejeita a chamada com 422 — não há
fallback de agente estático neste caminho (um número ou chave no modo webhook
não tem um agente atribuído).
Limite de tamanho da resposta
Exemplo de manipulador
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
WEBHOOK_SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]
def verify(body: bytes, signature: str) -> bool:
expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
@app.post("/thunderphone-webhook")
async def webhook(request: Request):
body = await request.body()
if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
raise HTTPException(status_code=401)
event = json.loads(body)
if event["type"] == "telephony.incoming":
caller = event["data"]["from_number"]
prompt = (
"Greet the caller as a San Francisco local…"
if caller.startswith("+1415")
else "You are a friendly customer support agent…"
)
return {
"prompt": prompt,
"voice": "john",
"product": "spark",
}
if event["type"] == "web.incoming":
return {
"prompt": "You are the website's helpful voice assistant…",
"voice": "john",
"product": "spark",
}
return {}import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;
function verify(body, signature) {
const expected = crypto
.createHmac("sha256", SECRET)
.update(body)
.digest("hex");
return signature &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
app.post(
"/thunderphone-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "telephony.incoming" || event.type === "web.incoming") {
const caller = event.data.from_number || "web";
const prompt = caller.startsWith("+1415")
? "Greet the caller as a San Francisco local…"
: "You are a friendly customer support agent…";
return res.json({
prompt,
voice: "john",
product: "spark",
});
}
res.json({});
},
);Resposta com ferramentas de função
Anexe ferramentas para que a IA possa chamar suas APIs durante a conversa:
{
"prompt": "You are a booking assistant. Use the available tools to help customers schedule appointments.",
"voice": "john",
"product": "spark",
"tools": [
{
"type": "function",
"function": {
"name": "search_appointments",
"description": "Find available appointment slots",
"parameters": {
"type": "object",
"properties": {
"date": { "type": "string", "description": "YYYY-MM-DD" },
"service": { "type": "string" }
},
"required": ["date"]
}
},
"endpoint": {
"url": "https://api.example.com/appointments/search",
"method": "POST",
"headers": {
"X-Api-Key": "your-key"
}
}
}
]
}Guia rápido de níveis de produto
| Produto | Latência | Raciocínio | Confirmação |
|---|---|---|---|
spark | Mais baixa | Básico | — |
bolt | Baixa | Aprimorado | — |
storm-base | Média | Forte | — |
storm-base-with-ack | Média | Forte | Preenchimento automático enquanto pensa |
storm-extra | Mais alta | Profundo | — |
storm-extra-with-ack | Mais alta | Profundo | Preenchimento automático enquanto pensa |
Relacionados
O evento não bloqueante de encerramento da chamada.
Esquema JSON completo para tools[] e o contrato de endpoint assinado.
Inscreva várias URLs em telephony.incoming / web.incoming.
Padrões para prompts, ferramentas e testes A/B por quem liga.