Skip to main content

Webhooks entrantes

Enrutamiento dinámico de agentes: Finn llama a tu servidor cuando llega una llamada entrante.

7 min read

Webhooks entrantes

Cuando llega una llamada entrante, Finn puede preguntar a tu servidor en tiempo real con qué agente responder, qué variables inyectar y qué base de conocimiento cargar. Esto te permite enrutar de forma dinámica: por número del llamante, nivel de cuenta, hora del día, cohorte de A/B o cualquier lógica que le importe a tu backend.

Si con un agente fijo te basta, omite esta página. Si quieres enrutamiento dinámico por llamada, esta es la página.


Cómo funciona

caller dials → Finn answers ring → Finn POSTs to your URL → your server returns agent + vars → Finn streams the voice agent

El intercambio añade unos 100-200 ms de latencia. Tu endpoint debe responder en menos de 500 ms o Finn recurrirá al agente predeterminado.


Configurar

1. Define la URL del webhook entrante

Panel → Configuración → Números de teléfono → [número] → URL del webhook entrante.

Pega tu endpoint HTTPS. Guarda. Finn lo consulta una vez con un cuerpo {"ping": true} para verificar que es accesible.

Mediante API

curl https://api.hirefinn.ai/v1/phone-numbers/ph_1234 \
  -X PATCH \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -d '{ "inbound_webhook_url": "https://your-app.com/finn/inbound" }'

2. Define un agente de respaldo

Configura un Finn predeterminado para ese número: se usa si tu webhook agota el tiempo de espera, falla o devuelve una respuesta no válida.


Carga útil de la solicitud

Finn envía un POST a tu URL con:

{
  "session_id": "ws_2H4abc",
  "request_type": "inbound",
  "phone_number_id": "ph_1234",
  "channel": "pstn",
  "from": {
    "phone": "+919876543210",
    "country": "IN",
    "carrier": "Airtel"
  },
  "to": {
    "phone": "+918765432100",
    "country": "IN"
  },
  "received_at": "2026-05-22T14:30:00Z",
  "metadata": {}
}

En las llamadas de WhatsApp, el bloque from usa campos específicos de WhatsApp:

"from": {
  "wa_id": "919876543210",
  "display_name": "Priya M",
  "profile_pic_url": "https://..."
},
"channel": "whatsapp"

Respuesta

Devuelve un cuerpo JSON en menos de 500 ms que describa cómo enrutar la llamada:

{
  "finn_id": "fn_def456",
  "language": "hi-IN",
  "variables": {
    "customer_name": "Priya M",
    "account_tier": "premium",
    "last_order_id": "ord_99831",
    "preferred_agent": "Aria"
  },
  "knowledge_base_ids": ["kb_general", "kb_premium_perks"],
  "metadata": {
    "campaign_tag": "premium-support-q2",
    "ab_cohort": "B"
  },
  "recording_enabled": true,
  "max_call_duration_seconds": 600
}

Referencia de campos

Campo¿Obligatorio?Notas
finn_idQué agente responde. Debe pertenecer a tu organización.
languageNoAnula el idioma predeterminado del agente. en-US, hi-IN, es-ES, etc.
variablesNoConjunto libre de pares clave/valor. Disponible dentro del prompt como {variable_name}.
knowledge_base_idsNoAnula qué bases de conocimiento se cargan para esta llamada.
metadataNoSe adjunta al registro de la llamada y al webhook posterior a la llamada. Úsalo para tu propio etiquetado analítico.
recording_enabledNoAnula el valor predeterminado del agente.
max_call_duration_secondsNoLímite máximo. El valor predeterminado es 1800 (30 min).

Rechazar la llamada

{ "action": "reject", "reason": "blocked_caller" }

Finn finaliza la llamada con un mensaje de salida configurado ("Este número ya no está en servicio"). Úsalo para aplicar la lista DNC, cuentas bloqueadas o cortes fuera de horario.

Transferir de inmediato

{ "action": "transfer", "to": "+918888888888", "reason": "vip_route" }

Omite por completo el agente de IA y dirige la llamada directamente a una persona. Útil para niveles VIP o de escalamiento.


Ejemplo de implementación (Node)

import express from "express";

const app = express();
app.use(express.json());

app.post("/finn/inbound", async (req, res) => {
  const { from, to, channel } = req.body;

  // 1. Look up caller in CRM (must be fast — DB index by phone)
  const phone = from.phone ?? `+${from.wa_id}`;
  const customer = await crm.findByPhone(phone);

  // 2. Block known DNC
  if (customer?.dnc) {
    return res.json({ action: "reject", reason: "dnc" });
  }

  // 3. VIP → straight to human
  if (customer?.tier === "vip") {
    return res.json({
      action: "transfer",
      to: process.env.VIP_DESK_NUMBER!,
      reason: "vip_route",
    });
  }

  // 4. Localized agent based on caller country
  const language = from.country === "IN" ? "hi-IN" : "en-US";

  // 5. Pick agent + load context
  return res.json({
    finn_id: customer?.tier === "premium"
      ? "fn_premium_aria"
      : "fn_standard_aria",
    language,
    variables: {
      customer_name: customer?.name ?? "there",
      account_tier: customer?.tier ?? "standard",
      last_order_id: customer?.last_order_id ?? "",
    },
    knowledge_base_ids:
      customer?.tier === "premium" ? ["kb_general", "kb_premium"] : ["kb_general"],
    metadata: { ab_cohort: customer?.ab_cohort ?? "A" },
  });
});

app.listen(3000);

Verificación de firma

Mismo esquema que los webhooks posteriores a la llamada: HMAC-SHA256 sobre el cuerpo sin procesar, cabecera X-Finn-Signature.

const sig = req.headers["x-finn-signature"] as string;
const ok = verifyFinnSignature(rawBody, sig, process.env.FINN_INBOUND_SECRET!);
if (!ok) return res.status(401).send("bad signature");

Consulta Webhooks posteriores a la llamada para ver la implementación completa de la verificación.


Requisitos de rendimiento

Esto está en la ruta crítica: quien llama escucha el tono de llamada mientras espera tu respuesta.

MétricaObjetivo
Tiempo de respuesta< 500 ms (p99)
Tiempo de espera1000 ms
Alternativa al agotarse el tiempo de esperaAgente predeterminado configurado en el número de teléfono
Alternativa ante un error 5xxAgente predeterminado
Alternativa ante JSON no válidoAgente predeterminado y error registrado

Consejos para bajar de 500 ms

  • Almacena en caché las consultas al CRM por número de teléfono durante 5 minutos
  • Usa una base de datos colocalizada: webhook desde us-east y base de datos en us-west suman 80 ms por trayecto
  • Precalcula las decisiones de enrutamiento: guarda la respuesta en el registro del cliente en lugar de decidirla en vivo
  • Omite el enriquecimiento no esencial en el salto entrante; deja ese trabajo para el webhook posterior a la llamada

Preselección de la persona que llama

La mayoría de las aplicaciones usan el webhook entrante para uno de estos patrones:

PatrónCaso de uso
Búsqueda + personalizaciónObtiene el registro del cliente y pasa el nombre y el nivel como variables
Aplicación de la lista DNCRechaza llamadas de números bloqueados
Enrutamiento VIPOmite el agente y transfiere directamente a un operador humano
Detección de idiomaElige hi-IN para India y en-US para quienes llaman desde EE. UU.
Pruebas A/BEnruta el 50 % de las llamadas a una nueva versión del agente
Enrutamiento fuera de horarioAgente distinto (o buzón de voz) fuera del horario laboral
Seguimiento de campañasEtiqueta las llamadas de números de seguimiento concretos con un ID de campaña
SaaS multiinquilinoEnruta al inquilino cuyo número se marcó

Pruebas

Usa la CLI de Finn para simular llamadas entrantes sin un timbre real por PSTN:

finn inbound simulate \
  --phone-number-id ph_1234 \
  --from +919876543210 \
  --webhook-url https://your-app.com/finn/inbound

La salida muestra la respuesta de tu endpoint, la latencia y el agente y las variables resueltos. Compatible con CI.


Problemas frecuentes

SíntomaSolución
Las llamadas siempre recurren al agente predeterminadoTu endpoint tarda más de 500 ms. Revisa el registro de webhooks de Finn para ver los tiempos.
Quien llama oye 2-3 s de silencio antes de que hable el agenteTu endpoint es lento pero no supera el tiempo de espera. Optimízalo a menos de 200 ms.
Las variables no aparecen en el habla del agenteDiscrepancia entre el marcador de posición {customer_name} del prompt y la clave customerName del webhook. Usa snake_case en ambos lados.
Respondió el agente equivocadoTu endpoint devolvió un finn_id que pertenece a otra organización. Verifica la propiedad.
action: reject no se respetaComprueba que reason sea una cadena y que el JSON de respuesta sea válido. Las respuestas no válidas → recurren al fallback.

Relacionado

Was this page helpful?

Still stuck or have feedback?

Email [email protected] or use the chat bubble in the bottom-right corner — it's a Finn that knows the Academy cold.