Ir al contenido

Webhooks

Un webhook es una dirección de tus sistemas a la que Caller envía el resultado de cada llamada en cuanto queda analizada. Es la forma de llevar puntuaciones, criterios y resúmenes a tu CRM, a tu almacén de datos o a un panel propio sin que nadie exporte nada.

  1. En la app, ve a Integraciones y, en Avisos, pulsa Configurar en la tarjeta Webhooks.

  2. Escribe la Dirección que recibirá los envíos y deja marcado Enviar los resultados.

  3. Guarda. Caller muestra el secreto de firma, que empieza por whsec_. Cópialo: no se vuelve a mostrar.

  4. Pulsa Enviar una prueba para comprobar que tu dirección responde.

Hay un webhook por cuenta. La dirección tiene que cumplir tres condiciones:

  • Empezar por https://.
  • Ser pública. No valen localhost, direcciones IP privadas ni nombres internos como servidor.local.
  • No llevar usuario ni contraseña dentro (https://usuario:clave@…).

Para pausar los envíos sin perder la configuración, desmarca Enviar los resultados.

Cada envío es un POST con el cuerpo en JSON y estas cabeceras:

Cabecera Valor
Content-Type application/json
User-Agent Caller-Webhooks/1
X-Caller-Event El tipo de evento: call.analyzed o ping.
X-Caller-Signature La firma del cuerpo. Ver comprobar la firma.

Se envía cada vez que termina el análisis de una llamada, venga de la API o de una subida a mano. Si una llamada se vuelve a analizar, se envía otra vez con el resultado nuevo.

{
"event": "call.analyzed",
"sent_at": "2026-10-09T08:51:37.204Z",
"call": {
"id": "k57e2xq9m4hc8w1t6b0z5y2e97c4n1ad",
"external_id": "pbx-000123",
"url": "https://app.caller.ee/calls/k57e2xq9m4hc8w1t6b0z5y2e97c4n1ad",
"title": "Pide la baja por el precio y acepta un descuento",
"score": 76,
"band": "mid",
"topic": "Baja del servicio",
"outcome": "resolved",
"customer_sentiment": { "start": "negative", "end": "neutral" },
"agent": {
"id": "jd7b4m1n8p2q5r9s3t6v0w4x7y1z8c2e",
"name": "Lucía Romero",
"code": "AG1143"
},
"campaign": "Retención",
"started_at": "2026-10-09T08:42:11.000Z",
"duration_seconds": 312,
"needs_review": true,
"review_reasons": ["required_failed"],
"criteria": [
{ "key": "saludo", "name": "Saludo corporativo", "score": 10, "max": 10, "required": false },
{ "key": "verificacion", "name": "Verificación de identidad", "score": 0, "max": 10, "required": true },
{ "key": "alternativa", "name": "Ofrece una alternativa a la baja", "score": 20, "max": 20, "required": false },
{ "key": "cierre", "name": "Cierre de la llamada", "score": 8, "max": 10, "required": false }
],
"summary": "La clienta llama para darse de baja por la última subida de precio. La agente le ofrece un descuento durante seis meses y la clienta lo acepta. No se verifica su identidad antes de consultar el contrato.",
"next_step": "Aplicar el descuento en la próxima factura."
}
}
Campo Tipo Descripción
event texto call.analyzed.
sent_at fecha Cuándo se hizo este envío, en ISO 8601 y UTC. Cambia en cada reintento.
call.id texto El identificador de la llamada en Caller.
call.external_id texto o null El identificador que enviaste por la API. null en las llamadas subidas a mano.
call.url texto La dirección de la llamada en la app.
call.title texto El título que le ha puesto el análisis.
call.score número o null La puntuación, de 0 a 100. null si la plantilla no tiene criterios que puntúen.
call.band texto o null El tramo de la puntuación: good (80 o más), mid (de 60 a 79) o bad (menos de 60).
call.topic texto o null El tema principal.
call.outcome texto o null Cómo terminó: resolved, follow_up, escalated o unresolved.
call.customer_sentiment objeto o null El sentimiento del cliente al empezar (start) y al terminar (end): positive, neutral o negative.
call.agent objeto o null El agente: id, name y code. Cualquiera de los tres puede ser null.
call.campaign texto o null El nombre de la campaña.
call.started_at fecha o null Cuándo empezó la llamada, en UTC.
call.duration_seconds número o null La duración de la grabación.
call.needs_review booleano Si la llamada está en la cola Para revisar en el momento del envío.
call.review_reasons lista Los motivos por los que pide una escucha: required_failed, critical_score, customer_upset, unresolved, long_silence. Vacía si no hay ninguno. Puede traer motivos con needs_review en false si alguien ya la marcó como revisada.
call.criteria lista Un elemento por criterio de la plantilla: su clave (key), su nombre, los puntos conseguidos (score), los posibles (max) y si es obligatorio (required).
call.summary texto o null El resumen de la llamada.
call.next_step texto o null El próximo paso acordado, si lo hay.

La key de un criterio no cambia aunque le cambies el nombre en la plantilla. Úsala para relacionar los resultados de una semana con los de la siguiente.

El cuerpo no incluye la transcripción ni el audio. Para verlos, abre call.url.

Es lo que envía el botón Enviar una prueba. Sirve para comprobar la conexión y la firma sin esperar a una llamada real.

{ "event": "ping", "sent_at": "2026-10-09T08:40:02.118Z" }

Responde con un código 2xx en menos de 10 segundos. El contenido de tu respuesta no se lee.

Haz lo mínimo antes de responder: comprueba la firma, guarda el cuerpo y contesta. Lo que tarde, como escribir en un CRM o recalcular un informe, hazlo después. Un receptor que tarda más de 10 segundos cuenta como un envío fallido aunque acabe procesándolo.

Tu dirección es pública, así que cualquiera puede enviarle un POST. La firma demuestra que un envío viene de Caller y que nadie ha tocado su contenido.

La cabecera X-Caller-Signature tiene esta forma:

X-Caller-Signature: sha256=5a1f0c…

Lo que sigue a sha256= es el HMAC-SHA256 del cuerpo de la petición, calculado con tu secreto de firma y escrito en hexadecimal. Para comprobarlo, calcula el mismo HMAC con el cuerpo que has recibido y compara.

import hashlib
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
SECRETO = os.environ["CALLER_WEBHOOK_SECRET"].encode()
def firma_valida(cuerpo: bytes, cabecera: str) -> bool:
esperada = "sha256=" + hmac.new(SECRETO, cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, cabecera)
@app.post("/hooks/caller")
def recibir():
cuerpo = request.get_data() # los bytes tal como han llegado
if not firma_valida(cuerpo, request.headers.get("X-Caller-Signature", "")):
abort(401)
evento = request.get_json()
if evento["event"] == "call.analyzed":
guardar(evento["call"]) # tu código
return "", 204

Compara con una función de tiempo constante (hmac.compare_digest, timingSafeEqual, hash_equals) y no con ==.

En la configuración del webhook, marca Generar un secreto nuevo y guarda. El secreto anterior deja de valer en ese momento, así que los envíos que lleguen hasta que actualices tu sistema fallarán la comprobación. Hazlo en un momento de poco tráfico.

Un envío se da por fallido cuando tu dirección:

  • no responde en 10 segundos o no se puede conectar con ella,
  • responde con un código 5xx,
  • responde 429.

En esos casos Caller lo intenta dos veces más: al cabo de 1 minuto y, si vuelve a fallar, 10 minutos después. Tras el tercer intento, ese envío se abandona.

Cualquier otra respuesta se da por definitiva y no se reintenta. Eso incluye los 4xx y también las redirecciones: Caller no sigue un 301 ni un 302, así que configura la dirección final.

La tarjeta Webhooks de Integraciones muestra el resultado del último envío: aceptado, sin respuesta o el código que devolvió tu dirección.

  • Trata cada envío como «este es el estado actual de la llamada». Guarda el resultado con call.id como clave y sobrescribe si ya existía. Así, un reintento que llega dos veces o un nuevo análisis de la misma llamada dejan tus datos correctos sin lógica especial.
  • No des por hecho el orden. Un reintento puede llegar después del envío de otra llamada posterior.
  • Ignora los eventos que no conozcas. Si event no es uno de los que tratas, responde 2xx y no hagas nada.
  • Ten un plan para lo que se pierde. Si tu sistema está caído más de unos minutos, los envíos de ese rato se abandonan y no se repiten. Los resultados siguen en la app: puedes recuperarlos con una exportación a CSV o volviendo a analizar esas llamadas, lo que gasta análisis.

El webhook se envía cuando una llamada queda analizada. No se envía nada si:

  • La llamada no se pudo transcribir.
  • La cuenta no tiene plantilla, o no quedan análisis en el periodo. El evento llegará cuando la llamada se analice.
  • Alguien corrige una puntuación a mano en la app. La corrección no genera un envío nuevo.

Como la dirección tiene que ser pública y https, un servidor en localhost no sirve directamente. Mientras desarrollas, usa un túnel que publique tu puerto local con una dirección https temporal, configura esa dirección en Caller y pulsa Enviar una prueba.