Webhooks
Los webhooks permiten que tus sistemas reciban notificaciones HTTP POST en tiempo real cuando ocurren eventos de cola — un turno se une, es llamado, se completa, etc.
Primeros pasos
Sección titulada «Primeros pasos»- Abre admin.jonot.io/settings/integrations.
- Haz clic en Añadir endpoint.
- Introduce una URL HTTPS pública en la que escuche tu servidor.
- Elige a qué eventos quieres suscribirte.
- Haz clic en Guardar.
Guarda tu secreto de firma — se muestra una sola vez. Para comprobar que tu endpoint es accesible, usa el botón Enviar solicitud de prueba en la página de edición del endpoint.
Tipos de evento
Sección titulada «Tipos de evento»| Nombre del evento | Cuándo se dispara |
|---|---|
ticket.joined | Un cliente se une a una cola |
ticket.called | Un miembro del personal llama a un turno en el mostrador |
ticket.completed | Un turno se marca como completado |
ticket.cancelled | Un cliente o miembro del personal cancela un turno |
ticket.skipped | Se omite un turno (un aplazamiento que se puede volver a llamar) |
ticket.no_show | Se confirma que un turno llamado u omitido es una ausencia |
queue.status_changed | Cambia el estado de una cola (ACTIVE, PAUSED o CLOSED) |
Formato del payload
Sección titulada «Formato del payload»Cada entrega es un HTTP POST con:
Content-Type: application/jsonX-Jonot-Event: ticket.calledX-Jonot-Delivery-Id: <uuid>X-Jonot-Timestamp: 2024-06-01T12:00:00.000ZX-Jonot-Signature: v1,<base64-hmac-sha256>X-Jonot-Payload-Version: v1El cuerpo predeterminado es un sobre JSON. La forma del campo payload varía según el evento:
ticket.joined
{ "deliveryId": "<uuid>", "event": "ticket.joined", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "ticket": { "id": "tkt_…", "queueId": "q_…" }, "queue": { "id": "q_…", "name": "Main Queue" }, "location": { "id": "loc_…", "name": "Downtown" } }}ticket.called / ticket.completed / ticket.skipped / ticket.no_show
{ "deliveryId": "<uuid>", "event": "ticket.called", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "ticket": { "id": "tkt_…", "number": 42, "status": "CALLED", "queueId": "q_…", "createdAt": "2024-06-01T11:58:00.000Z", "updatedAt": "2024-06-01T12:00:00.000Z" } }}ticket.cancelled
{ "deliveryId": "<uuid>", "event": "ticket.cancelled", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "ticketId": "tkt_…", "queueId": "q_…" }}queue.status_changed
{ "deliveryId": "<uuid>", "event": "queue.status_changed", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "queueId": "q_…", "status": "PAUSED" }}Verificando la firma
Sección titulada «Verificando la firma»Jonot firma cada entrega con HMAC-SHA256 sobre la cadena:
<deliveryId>.<timestamp>.<body>Para verificar en Node.js (≥18):
import { createHmac, timingSafeEqual } from "node:crypto";
/** * Devuelve true cuando el encabezado de firma es válido y la marca de tiempo * está a menos de 5 minutos de la hora actual. Lanza un error si la entrada tiene un formato incorrecto. */function verifySignature(secret, deliveryId, timestamp, body, header) { // Protección contra ataques de repetición: rechaza entregas de más de 5 minutos. const ageMs = Date.now() - new Date(timestamp).getTime(); if (Math.abs(ageMs) > 5 * 60 * 1000) return false;
const expected = "v1," + createHmac("sha256", secret) .update(`${deliveryId}.${timestamp}.${body}`) .digest("base64");
// timingSafeEqual evita ataques de temporización (timing oracle). // Los buffers deben tener la misma longitud — si las longitudes difieren la firma // no es válida, pero igualmente comparamos un valor ficticio para mantener el tiempo constante. const expectedBuf = Buffer.from(expected); const headerBuf = Buffer.from(header); if (expectedBuf.length !== headerBuf.length) return false; return timingSafeEqual(expectedBuf, headerBuf);}Plantillas de payload
Sección titulada «Plantillas de payload»Puedes reemplazar el cuerpo JSON predeterminado por una plantilla personalizada. Las plantillas usan una sintaxis Mustache simplificada:
{{ path.to.value }}— se sustituye y se escapa según el tipo de contenido: escapado como cadena JSON paraapplication/json, codificado como porcentaje paraapplication/x-www-form-urlencoded, sin cambios paratext/plain{{{ path.to.value }}}— se sustituye sin escapar (sin escape)
Ejemplo de plantilla para application/json (suscrita a ticket.joined):
{ "type": "{{ event }}", "ticketId": "{{ payload.ticket.id }}", "queueName": "{{ payload.queue.name }}"}Editor de plantillas
Sección titulada «Editor de plantillas»El campo de la plantilla de payload es un editor de código completo con:
- Resaltado de sintaxis y coincidencia de paréntesis para plantillas JSON.
- Paleta de variables — una fila siempre visible de botones de inserción, uno por cada ruta de variable disponible para los eventos que has seleccionado. Haz clic en un botón para insertar un token
{{ path }}en la posición del cursor. - Validación en vivo — mientras escribes, el editor comprueba tanto la estructura JSON (cuando el tipo de contenido es
application/jsony no hay etiquetas{{{ }}}sin escapar) como las rutas de variables. Los diagnósticos aparecen como marcadores en línea en el editor y como un banner resumen debajo:- Error — una ruta que es desconocida en todos los eventos seleccionados.
- Advertencia — una ruta que solo existe para algunos de los eventos seleccionados (estará vacía en las entregas de los demás eventos).
La validación también se ejecuta al guardar en el servidor — el feedback del editor refleja exactamente las reglas del servidor.
Reintentos de entrega
Sección titulada «Reintentos de entrega»Las entregas fallidas (respuesta no 2xx o error de conexión) se reintentan hasta 3 veces con retroceso exponencial mediante Cloudflare Queues. Después de 3 fallos, la entrega pasa a la cola de mensajes fallidos (dead-letter) y el contador de fallos consecutivos del endpoint se incrementa.
Cuando un endpoint acumula 20 fallos consecutivos se desactiva automáticamente. Vuelve a activarlo desde la página de edición del endpoint; el contador se restablece a cero.
Historial de entregas
Sección titulada «Historial de entregas»La pestaña Entregas de cada endpoint muestra los últimos 30 días de intentos de entrega: tipo de evento, estado HTTP, número de intentos y marca de tiempo. Usa el botón Cargar más para navegar por registros anteriores.
Límites
Sección titulada «Límites»| Límite | Valor |
|---|---|
| Endpoints por organización | 5 |
| Encabezados personalizados por endpoint | 10 |
| Longitud del valor del encabezado | 1024 bytes |
| Tamaño de la plantilla de payload | 16 KB |
| Tiempo de espera de entrega | 10 s |
| Retención del historial de entregas | 30 días |
| Tasa máxima de entrega por organización | 120 / 60 s |
Rotar el secreto de firma
Sección titulada «Rotar el secreto de firma»- Abre la página de edición del endpoint.
- Haz clic en Rotar secreto de firma.
- Confirma la rotación en el diálogo.
- Copia el nuevo secreto de inmediato — se muestra una sola vez y no se puede recuperar. Si cierras el diálogo sin guardarlo, tendrás que rotar de nuevo para obtener un nuevo valor en texto plano.
- Actualiza tu servidor para que verifique las firmas con el nuevo secreto.
No hay ventana de superposición. El secreto anterior deja de ser válido en cuanto confirmas la rotación. Planea desplegar el nuevo secreto en tu receptor inmediatamente después de rotar.
La interfaz muestra Secreto activo: ····XXXX (los últimos cuatro caracteres) junto al botón Rotar, para que puedas comprobar qué secreto está vigente en cada momento — útil para confirmar que tu receptor y el servidor están sincronizados después de una rotación.
Higiene del secreto de firma
Sección titulada «Higiene del secreto de firma»Cuándo rotar:
- Sospecha de compromiso: el secreto apareció en un archivo de registro, se compartió con un empleado que dejó la empresa, o quedó capturado en una grabación de pantalla.
- Higiene rutinaria: rotarlo periódicamente limita el alcance de una filtración no detectada.
- Después de cualquier cambio en qué servicios pueden leer el secreto (rotación de la gestión de claves).
Cómo actualizar el receptor:
- Rota el secreto en la interfaz de administración y copia el nuevo valor.
- Actualiza el secreto en el almacén de secretos de tu receptor (variable de entorno, gestor de secretos, etc.).
- Despliega el receptor actualizado.
- Confirma que la siguiente entrega se realiza correctamente en la pestaña Entregas.
Si perdiste el nuevo secreto antes de guardarlo:
Rota de nuevo. Cada rotación genera un nuevo secreto aleatorio. El texto plano anterior no se puede recuperar — solo se almacena una forma cifrada con AEAD en el servidor.