Webhooks: avisos en vivo
En vez de preguntar cada minuto, el webhook le avisa a tu sistema apenas pasa. Firmado, con reintentos y con un registro de lo que mandamos.
Para qué sirve
Preguntar por los contactos que cambiaron funciona bien. Preguntar si terminó una llamada, no: cuando te enterás, el agente ya está en la siguiente. El aviso llega en el momento.
El webhook se crea en Configuración → API, en la pestaña Webhooks. Elegís la URL y los eventos que te interesan, y la clave de firma se muestra una sola vez.
Qué eventos hay
Elegís evento por evento al crear el webhook. El nombre de la izquierda es el que viaja en el campo type del cuerpo y en el encabezado DialSprint-Event.
El último no se elige en esta lista: lo manda la automatización que se configura en la tipificación, y llega a tu URL igual que cualquier otro.
call.completedLlamada terminadaTerminó una llamada, con su resultado. Una sola por conversación: las patas de una transferencia no son llamadas nuevas.
call.wrapped_upLlamada tipificadaEl agente cargó la tipificación y las notas. Es el que importa, porque es el que trae el resultado de la gestión. También llega cuando termina una llamada saliente estacionada que nadie retomó a tiempo, y en ese caso viene sin tipificación ni notas.
contact.createdContacto creadoSe cargó un contacto, en el panel o por la API. Una carga masiva NO dispara uno por fila.
contact.updatedContacto modificadoCambió algo de la ficha, incluido el bloque de cumplimiento que la API deja leer pero no escribir.
contact.archivedContacto archivadoEl contacto salió de circulación. Sigue existiendo y sigue viniendo en GET /contacts, con su archived_at.
contact_import.completedImportación terminadaTerminó una carga masiva, con los totales de creados, actualizados y fallidos.
disposition.appliedTipificación con automatizaciónEl agente eligió una tipificación que tiene configurada una automatización de webhook apuntando a este endpoint. Este no se elige acá: se configura en la tipificación, y sirve para enterarte solo de los resultados que te importan en vez de recibir todas las llamadas.
Cómo viene el aviso
Un POST con el mismo cuerpo que te devolvería el endpoint de ese recurso: si ya parseás GET /calls, no tenés que parsear nada nuevo.
El id del evento sirve para descartar repetidos. Podemos reintentar, así que tu receptor tiene que aguantar recibir el mismo aviso dos veces.
{
"id": "evt_01m2xem2esr7k9a0ar29edt9wj",
"type": "call.wrapped_up",
"created_at": "2026-09-20T14:05:20.000000Z",
"data": {
"id": "01M2XC7N8P0Q1R2S3T4U5V6W7X",
"direction": "outbound",
"status": "completed",
"outcome": "answered",
"contact_id": "01M2XEM2ESR7K9A0AR29EDT9WJ",
"disposition": { "id": "01M2XD...", "name": "Interesado", "category": "close", "outcome": "answered" },
"notes": "Confirma el viernes",
"duration_seconds": 184,
"has_recording": true
}
}Comprobar la firma
Cada pedido lleva tres encabezados. El que importa es la firma: es un HMAC SHA-256 sobre el timestamp, un punto y el cuerpo, con el cuerpo tal como llegó y sin volver a serializarlo.
El timestamp va adentro de lo firmado a propósito: sin él, un cuerpo capturado una vez se puede reenviar para siempre. Rechazá lo que tenga más de cinco minutos.
DialSprint-Signature: t=1789056320,v1=5f2c8b...
DialSprint-Event: call.wrapped_up
DialSprint-Delivery: 01M2XF7N8P0Q1R2S3T4U5V6W7XEn código
Comparalo contra el valor del encabezado con una comparación de tiempo constante, no con un igual común.
# El valor firmado es "<t>.<cuerpo crudo>", con el cuerpo tal cual llegó.
printf '%s.%s' "$T" "$RAW_BODY" \
| openssl dgst -sha256 -hmac "$DIALSPRINT_WEBHOOK_SECRET" -hexReintentos y cuando se apaga
Contestá 2xx apenas lo recibas y encolá el trabajo: si tardás más de 10 segundos lo damos por fallado. Reintentamos seis veces, espaciando cada vez más, hasta media hora.
Si tres avisos agotan todos sus reintentos, apagamos el webhook y te avisamos en el panel. Un rechazo suelto no cuenta: para eso están los reintentos. Volvés a activarlo cuando lo arreglaste, y desde el registro de entregas reenviás lo que se perdió.
La URL tiene que ser https y estar abierta a internet: una dirección interna (localhost, una IP privada) la rechazamos al guardarla y también al mandar.