Ir al contenido

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.

  1. Abre admin.jonot.io/settings/integrations.
  2. Haz clic en Añadir endpoint.
  3. Introduce una URL HTTPS pública en la que escuche tu servidor.
  4. Elige a qué eventos quieres suscribirte.
  5. 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.

Nombre del eventoCuándo se dispara
ticket.joinedUn cliente se une a una cola
ticket.calledUn miembro del personal llama a un turno en el mostrador
ticket.completedUn turno se marca como completado
ticket.cancelledUn cliente o miembro del personal cancela un turno
ticket.skippedSe omite un turno (un aplazamiento que se puede volver a llamar)
ticket.no_showSe confirma que un turno llamado u omitido es una ausencia
queue.status_changedCambia el estado de una cola (ACTIVE, PAUSED o CLOSED)

Cada entrega es un HTTP POST con:

Content-Type: application/json
X-Jonot-Event: ticket.called
X-Jonot-Delivery-Id: <uuid>
X-Jonot-Timestamp: 2024-06-01T12:00:00.000Z
X-Jonot-Signature: v1,<base64-hmac-sha256>
X-Jonot-Payload-Version: v1

El 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"
}
}

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);
}

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 para application/json, codificado como porcentaje para application/x-www-form-urlencoded, sin cambios para text/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 }}"
}

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/json y 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.

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.

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ímiteValor
Endpoints por organización5
Encabezados personalizados por endpoint10
Longitud del valor del encabezado1024 bytes
Tamaño de la plantilla de payload16 KB
Tiempo de espera de entrega10 s
Retención del historial de entregas30 días
Tasa máxima de entrega por organización120 / 60 s
  1. Abre la página de edición del endpoint.
  2. Haz clic en Rotar secreto de firma.
  3. Confirma la rotación en el diálogo.
  4. 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.
  5. 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.

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:

  1. Rota el secreto en la interfaz de administración y copia el nuevo valor.
  2. Actualiza el secreto en el almacén de secretos de tu receptor (variable de entorno, gestor de secretos, etc.).
  3. Despliega el receptor actualizado.
  4. 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.