Open in
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 em uma chave publicável no
mode="webhook", o ThunderPhone envia uma solicitação bloqueante
telephony.incoming / web.incoming para sua
URL de webhook legada
e aguarda uma resposta de configuração: até 13 segundos para uma
chamada telefônica, incluindo uma tentativa adicional, e até 10 segundos para uma sessão
de widget (consulte Tempos limite e novas tentativas). Use este
intercâmbio 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.
Tempos limite e novas tentativas
Responda rápido: quem liga está aguardando na linha enquanto você decide.
Para uma chamada telefônica (telephony.incoming):
- A primeira tentativa pode levar até 10 segundos.
- Se exceder o tempo limite, não conseguir conectar ou receber uma resposta
5xx,408ou429, o ThunderPhone tenta mais uma vez. A nova tentativa recebe o tempo restante de um orçamento de 13 segundos para toda a troca: cerca de 3 segundos após uma primeira tentativa que excedeu o tempo limite, quase todos os 13 após uma falha rápida. Não há nova tentativa quando restariam menos de 2 segundos. - Qualquer outra resposta
4xx, uma resposta acima do limite de tamanho ou uma resposta2xxcujo corpo não seja uma configuração válida é final e não gera nova tentativa. - Ambas as tentativas enviam o mesmo corpo, a mesma
X-ThunderPhone-Signaturee o mesmo cabeçalhoX-ThunderPhone-Idempotency-Key(telephony-incoming-<call_id>), para que um manipulador com efeitos colaterais possa reconhecer a nova tentativa.
Não há agente de fallback. Se nenhuma configuração utilizável chegar, a
chamada é encerrada imediatamente, em vez de deixar quem liga em uma linha
silenciosa. Ela aparece no histórico de chamadas como uma chamada com falha e
não faturada, com end_reason inbound_config_webhook_failed, e o evento
usual telephony.complete é enviado.
Uma sessão de widget (web.incoming) recebe uma única tentativa de até 10
segundos. Se ela falhar, a solicitação de sessão falha com 502 ou 422.
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, conforme enviada pelo navegador. Um cliente que não seja um navegador pode enviar qualquer valor, portanto não o trate como prova da origem da solicitação |
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 da página por sessão |
Os campos de substituição (language, voice, website_context e quaisquer
variables do navegador) chegam somente quando a chave publicável os permite em
suas substituições do navegador. Uma
solicitação de sessão que contenha uma substituição não permitida pela chave é rejeitada antes que
este webhook seja chamado.
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 da voz de GET /v1/voices, por exemplo, john. voice_name é aceito como um 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 | booleano | não | Ativa a supervisão desta chamada. O padrão é false |
additional_audio_context | booleano | null | não | Inclui os últimos turnos do áudio de quem liga, em vez de apenas o turno mais recente, melhorando correções e a coleta de dados com muita soletração ou números, com pequeno impacto na latência e no custo. É ativado por padrão para sessões de entrada 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 de primary_language |
primary_language | string | não | Código de idioma normalizado (o padrão é en). Códigos que não podem ser resolvidos rejeitam a chamada |
has_additional_languages | booleano | não | O padrão é false |
additional_languages | array de strings | não | Idiomas adicionais para os quais o agente pode alternar |
native_voice_switching | booleano | não | O padrão é false. Quando a chamada alterna para outro idioma, troca para uma voz nativa desse idioma (correspondente ao gênero) em vez de manter a voz configurada |
language_switching | string | null | não | ask_first (padrão) ou follow_caller; null é igual a ask_first. Define como um agente multilíngue lida com a outra pessoa falando outro de seus idiomas. ask_first alterna imediatamente quando a pessoa pede o idioma ou diz uma frase completa nele e pergunta se deve continuar nesse idioma após algo curto, como um "sim". follow_caller alterna para qualquer idioma configurado que a pessoa fale, mesmo que seja uma única palavra. Aplica-se apenas quando additional_languages está definido |
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-with-ack) |
acknowledgement_prompt | string | não | Usado quando acknowledgement_prompt_mode="manual" |
silence_interval_seconds | inteiro | null | não | 5–120. Segundos de silêncio de quem liga antes de uma verificação |
silence_max_checkins | inteiro | null | não | 1–10 |
silence_checkins_enabled | booleano | não | O padrão é true |
connect_tone_enabled | booleano | não | O padrão é false |
pronunciation_dicts | booleano | null | não | O padrão é false; null é igual a false. Quando true, a fala da Cartesia na chamada usa o dicionário de pronúncia do ThunderPhone para o idioma da chamada, quando houver um disponível. Funciona para chamadas telefônicas e de widget. Aplica-se apenas às vozes Cartesia do próprio ThunderPhone, não às vozes na sua própria chave de API da Cartesia. Não é usado com agent_id, em que as configurações salvas do agente se aplicam |
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 embutidos de ferramentas de função (consulte Ferramentas de função) |
transfer_destinations | array de objetos | null | não | Até 50 entradas {name, number} para as quais esta chamada pode ser transferida, validadas como os números de transferência de um agente. O agente transfere pelo nome e somente para esses destinos; [] significa nenhuma transferência. Omita esse campo ou envie null para permitir que o agente transfira para um número que escolher |
timezone | string | não | Fuso horário IANA para esta chamada, como America/Chicago. O agente informa a data e a hora nesse fuso, em vez de usar o próprio fuso horário. Também permitido ao lado de agent_id. Valores que não são fusos IANA são ignorados. Consulte Definir o fuso horário por chamada |
call_id | inteiro | 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: uma chamada telefônica é encerrada
conforme descrito em Tempos limite e novas tentativas, e uma
solicitação de sessão do widget falha com 422. Não há alternativa para agente estático
neste caminho (um número ou uma chave no modo webhook não tem 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.