Saltar al contenido

Webhooks

Recibe notificaciones push en tiempo real cuando ocurren cosas en su cuenta de Quotery. Sin consultas repetitivas, sin trabajos programados, solo llamadas HTTP firmadas enviadas directamente a su servidor.

Qué son los webhooks?

Un webhook es una notificación push que Quotery envía a su servidor cada vez que ocurre algo importante. Se acepta una cotización, una nota de entrega se marca como entregada o se crea una nota de devolución. En lugar de que usted pregunte "algo nuevo?" una y otra vez (eso es consultas repetitivas, y es ineficiente), nosotros le llamamos en el momento en que se activa un evento.

Usted nos dice una URL y qué tipos de eventos le interesan. Cuando uno de esos eventos ocurre en su cuenta, hacemos un POST con un payload JSON firmado a su URL. Usted verifica la firma para asegurarse de que realmente vino de nosotros, procesa los datos y devuelve un 200. Eso es todo.

Los webhooks son la columna vertebral de las integraciones en tiempo real. Úselos para enviar pedidos a su ERP, sincronizar inventario con un sistema externo o disparar notificaciones de Slack cuando se cierra una cotización. Construya un registro de auditoria de todo lo que sucede en su empresa. Usted elige los eventos, usted controla el receptor.

Tipos de eventos disponibles

Cada tipo de evento es una acción de negocio específica dentro de Quotery. Se suscribe a los que son relevantes para su integración, elija algunos o los siete.

  • quote.closedSe activa cuando una cotización se cierra (finalizada y enviada al cliente). El payload incluye los detalles de la cotización, partidas y totales al momento del cierre.
  • quote.acceptedSe activa cuando un cliente acepta una cotización a través del portal del cliente. Este es el evento principal para iniciar el cumplimiento, crea un pedido en su ERP, notifique a su almacén o genere una factura de venta.
  • quote.cancelledSe activa cuando una cotización se cancela. Use esto para deshacer procesos posteriores, liberar inventario reservado, cancelar un pedido en borrador o actualizar la etapa del negocio en su CRM.
  • delivery_note.createdSe activa cuando se crea una nota de entrega. Si está rastreando envíos salientes en un sistema externo, este evento le dice que se está preparando una nueva entrega.
  • delivery_note.marked_deliveredSe activa cuando una nota de entrega se marca como entregada. Esta es la confirmación de que los bienes han llegado al cliente, úsala para reducir inventario, cerrar una tarea de cumplimiento o activar una factura.
  • stock_receipt.completedSe activa cuando se completa una recepción de existencias. Los bienes entrantes ahora están en su inventario. Sincronice esto con su sistema de gestión de almacén o actualice los niveles de existencias en un catálogo externo.
  • return_note.createdSe activa cuando se crea una nota de devolución. Un cliente está devolviendo algo, querrá ajustar el inventario, activar un flujo de reembolso o registrar la devolución en su sistema de gestión de pedidos.

Creando un webhook

Configurar un webhook toma alrededor de un minuto. Necesitará acceso de administrador a su cuenta de Quotery, la gestión de webhooks es una función administrativa.

  1. Elija un nombre y URL de destino
    Dele a su webhook un nombre legible, algo como "Integración ERP de Producción" para que su equipo sepa lo que hace. La URL de destino debe ser un endpoint HTTPS que acepte solicitudes POST con cuerpo JSON. Este es su servidor, o un servicio como Zapier, Make o un webhook entrante de Slack.
  2. Elija sus tipos de eventos
    Elija los eventos a los que quiere suscribirse. Puede seleccionar algunos (por ejemplo, solo quote.accepted y quote.closed) o los siete. Solo los eventos que marque activarán entregas a su URL. Puede actualizar esta lista más tarde sin crear un nuevo webhook.
  3. Crearlo a través de la API
    Los webhooks se administran a través de la API REST en /api/v1/webhooks/. Hará un POST con un cuerpo JSON con su nombre, URL y lista de eventos. La respuesta incluye un signing_secret, guárdelo de inmediato. Se muestra una sola vez y nunca más. Si lo pierde, necesitará crear un nuevo webhook.
  4. Almacena el secreto de firma de forma segura
    Trate el secreto de firma como una contraseña. Almacénelo como una variable de entorno en su servidor (QUOTERY_WEBHOOK_SECRET es un buen nombre). Lo usará para verificar cada entrega entrante, así que cualquiera con acceso a este secreto puede falsificar payloads de webhook.
  5. Verifica que las entregas están llegando
    Active un evento real en su cuenta de Quotery (cierre una cotización, marque una nota de entrega como entregada) y verifique que su endpoint reciba la solicitud POST. Puede inspeccionar la salud de entrega del webhook, última marca de tiempo exitosa y conteo de fallos, en cualquier momento a través del endpoint GET. Verde significa que está funcionando.

El secreto de firma se genera automáticamente al crear el webhook. Usted no lo elige, lo recibe. Y solo lo ve una vez, justo después de la creación. Cópielo en un lugar seguro antes de cerrar esa pestaña de respuesta.

Debe haber iniciado sesión como usuario administrador para crear o gestionar webhooks. Si no es administrador, contacte a alguien de su equipo que lo sea.

Verificando firmas

Cada entrega de webhook incluye una firma criptográfica para que pueda estar seguro de que el payload vino de Quotery y no ha sido alterado. Verificarla es un paso obligatorio, nunca procese un payload de webhook sin verificar la firma primero.

Firmamos el cuerpo de la solicitud sin procesar con HMAC-SHA256 usando el secreto de firma único de su webhook. La firma se envía en el encabezado X-Quotery-Signature en un formato compatible con el esquema de firma de webhooks de Stripe, así que si ha trabajado con webhooks de Stripe antes, esto le resultará familiar.

Formato de la firma

El encabezado X-Quotery-Signature se ve así:

t=1714789200,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
t
La marca de tiempo Unix (en segundos) cuando se generó la firma. Su código de verificación debe rechazar cualquier entrega con una marca de tiempo de más de 5 minutos de antigüedad; esto previene ataques de repetición.
v1
El resumen hexadecimal HMAC-SHA256 del cuerpo de la solicitud, calculado con su secreto de firma.

Python

import hmac, hashlib, time

def verify_signature(body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
 parts = dict(p.split("=", 1) for p in signature_header.split(","))
 timestamp = int(parts["t"])
 if abs(time.time() - timestamp) > tolerance:
 return False # outside tolerance window
 expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
 return hmac.compare_digest(parts["v1"], expected)

El parámetro de tolerancia (300 segundos por defecto = 5 minutos) rechaza entregas que son demasiado antiguas. Esta es su protección contra ataques de repetición. Ajústelo si sus relojes están desincronizados, pero no lo configure mucho más alto que 5 minutos.

Node.js

const crypto = require("crypto");

function verifySignature(rawBody, signatureHeader, secret, tolerance = 300) {
 // Parse "t=1714789200,v1=abc123..." into { t, v1 }
 const parts = {};
 signatureHeader.split(",").forEach(p => {
 const idx = p.indexOf("=");
 parts[p.slice(0, idx)] = p.slice(idx + 1);
 });

 const timestamp = parseInt(parts.t, 10);
 const now = Math.floor(Date.now() / 1000);

 // Reject if outside tolerance window (5 minutes)
 if (Math.abs(now - timestamp) > tolerance) {
 return false;
 }

 // Compute the expected signature
 const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");

 // Constant-time comparison prevents timing attacks
 return crypto.timingSafeEqual(
 Buffer.from(parts.v1),
 Buffer.from(expected)
 );
}

La verificación de firma use timingSafeEqual en lugar de una comparación de cadenas simple. Esto es importante, los operadores de igualdad regulares hacen cortocircuito, lo que filtra información de tiempo sobre cuanto de la firma coincidio. Siempre use una comparación de tiempo constante para la verificación HMAC.

Encabezados de entrega

Cada entrega POST incluye estos cuatro encabezados. Los usará para verificación de firma, idempotencia y decisiones de enrutamiento.

HeaderValue
Content-Typeapplication/json
X-Quotery-Webhook-IdUUID (ej. 550e8400-e29b-41d4-a716-446655440000)
X-Quotery-EventTipo de evento delimitado por puntos (ej. quote.closed)
X-Quotery-Signaturet=1714789200,v1=abc123...

Content-TypeEl cuerpo de la solicitud siempre es JSON. Su endpoint puede asumir esto de forma segura y analizarlo en consecuencia.

X-Quotery-Webhook-IdUn identificador único para este intento de entrega específico. Úsalo como clave de idempotencia, si una entrega se reintenta, llevará el mismo ID, por lo que puede deduplicar verificando si ya has procesado este ID de webhook.

X-Quotery-EventTe dice exactamente que evento se activo. Su endpoint puede usar este valor para enrutar diferentes eventos a diferentes manejadores.

X-Quotery-SignatureLa firma HMAC-SHA256. Analizala, verificala y solo procesa el payload si es valida.

Comportamiento de reintentos

Si su endpoint esta caido o devuelve un estado no 2xx, Quotery reintenta la entrega automáticamente. No necesita configurar nada, el programa de reintentos esta incorporado.

Programa de reintentos

Las entregas fallidas se reintentan hasta 6 veces usando retroceso exponencial. Aqui esta la línea de tiempo desde el intento de entrega inicial:

  • Attempt 1: 1 minuto después del primer fallo
  • Attempt 2: 5 minutos después del primer fallo
  • Attempt 3: 15 minutos después del primer fallo
  • Attempt 4: 1 hora después del primer fallo
  • Attempt 5: 4 horas después del primer fallo
  • Attempt 6: 12 horas después del primer fallo

La ventana complete de reintentos abarca aproximadamente 12 horas. Después de 6 intentos fallidos, la entrega se descarta permanentemente.

Monitoreando la salud de entrega

Cada webhook rastrea dos indicadores de salud: la marca de tiempo de la última entrega exitosa y un conteo consecutivo de fallos. Una entrega exitosa reinicia el conteo de fallos a cero. Una cadena de fallos lo incrementa. Puede verificar ambos en cualquier momento a través de la API, sin registros que revisar, sin alertas que configurar. Si ve un conteo de fallos en aumento, es probable que su endpoint este caido o rechazando entregas.

Como los reintentos reutilizan el mismo X-Quotery-Webhook-Id, su endpoint puede manejar entregas duplicadas de forma segura. Manten una pequena cache de IDs de webhook procesados recientemente (unas pocas horas son suficientes dada la ventana de reintentos de 12 horas) y omite cualquier entrega cuyo ID ya hayas visto.

Estructura del payload

Cada entrega de webhook lleva el mismo envoltorio externo. Los detalles específicos del evento viven dentro del objeto data.

{
 "event_type": "quote.closed",
 "tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
 "timestamp": "2026-05-03T10:00:00Z",
 "data": {
 // Payload específico del evento, varia segun event_type
 }
}
event_type
El tipo de evento delimitado por puntos de la lista anterior (ej. quote.closed). Use esto para enrutar el payload al manejador correcto en su integración.
tenant_id
El UUID de su empresa en Quotery. Si estas construyendo una integración que sirve a multiples empresas, use esto para limitar el procesamiento a la cuenta correcta.
timestamp
Una marca de tiempo ISO 8601 UTC de cuando se activo el evento. Útil para ordenar eventos o detectar entregas tardias.
data
El payload específico del evento. Para un evento de cotización, esto puede incluir el ID de la cotización, detalles del cliente, partidas y totales. Para un evento de nota de entrega, incluiria el ID de la nota de entrega, artículos y estado. La forma exacta depende del tipo de evento y la representacion serializada de la entidad en el momento en que se activo el evento.

Probando su webhook

No necesita un entorno de pruebas para probar webhooks. Puede activar eventos reales en su cuenta de Quotery y verlos llegar a su endpoint en tiempo real.

  1. Use webhook.site para pruebas iniciales
    webhook.site le da una URL temporal que registra cada solicitud entrante. Cree un webhook apuntando a una URL de webhook.site primero, esto le permite inspeccionar los encabezados exactos y la forma del payload antes de escribir cualquier código de receptor. Una vez que esté satisfecho con el formato, cambie la URL a su endpoint real.
  2. Active un evento real
    Realice la acción en Quotery que corresponde a su evento suscrito: cierre una cotización, marque una nota de entrega como entregada, complete una recepción de existencias. El webhook se activa automáticamente en el momento en que se guarda el evento, sin botones adicionales que presionar.
  3. Verifique la salud de entrega
    Haga GET a /api/v1/webhooks/ para ver last_success_at y failure_count de su webhook. Si last_success_at se actualizó y failure_count es cero, su endpoint recibió y confirmó la entrega. Si failure_count está aumentando, verifique que su endpoint sea accesible, devuelva 2xx y responda en menos de 10 segundos.
  4. Itere en su manejador
    Use los datos reales del payload para construir su lógica de integración. Analice event_type para enrutar al manejador correcto, extraiga los campos que necesita de data y mapéelos a su sistema externo. El patrón de encabezados y verificación de firma permanece igual independientemente del tipo de evento que esté manejando.

Ejemplos de código

Listo para conectarlo? Aquí están las dos tareas más comunes, crear un webhook y recibir entregas, en forma de código listo para copiar y pegar.

Creando un webhook con curl

Este comando curl crea un webhook que escucha los eventos quote.accepted, quote.closed y delivery_note.marked_delivered. Necesitará su cookie de sesión de una sesión de navegador autenticada como administrador.

curl -X POST https://app.quotery.io/api/v1/webhooks/ \
 -H "Content-Type: application/json" \
 -H "Cookie: sessionid=<su-id-de-sesión-admin>" \
 -d '{
 "name": "Mi Integración",
 "url": "https://miapp.example.com/webhooks/quotery",
 "events": ["quote.accepted", "quote.closed", "delivery_note.marked_delivered"]
 }'

La respuesta 201 incluye el ID del webhook y, fundamentalmente, el signing_secret. Copie ese secreto ahora. No lo verá de nuevo.

Endpoint receptor en Node.js

Aquí tiene un endpoint Express mínimo que verifica la firma, comprueba la tolerancia de marca de tiempo y deduplica por ID de webhook. Agréguelo a su aplicación Node existente o úselo como punto de partida.

const express = require("express");
const crypto = require("crypto");

const app = express();
const SECRET = process.env.QUOTERY_WEBHOOK_SECRET;
const TOLERANCE = 300; // 5 minutos en segundos

// Almacén de idempotencia en memoria, use Redis o una BD en producción
const processedIds = new Set();

app.post("/webhooks/quotery", express.json({
 verify: (req, _res, buf) => { req.rawBody = buf; }
}), (req, res) => {
 // 1. Analice el encabezado de firma
 const header = req.headers["x-quotery-signature"];
 if (!header) return res.status(400).send("Falta la firma");

 const parts = {};
 header.split(",").forEach(p => {
 const idx = p.indexOf("=");
 parts[p.slice(0, idx)] = p.slice(idx + 1);
 });

 // 2. Rechace si la marca de tiempo está fuera de tolerancia
 const timestamp = parseInt(parts.t, 10);
 if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > TOLERANCE) {
 return res.status(400).send("Marca de tiempo fuera de tolerancia");
 }

 // 3. Verifique la firma HMAC
 const expected = crypto
.createHmac("sha256", SECRET)
.update(req.rawBody)
.digest("hex");

 const valid = crypto.timingSafeEqual(
 Buffer.from(parts.v1),
 Buffer.from(expected)
 );
 if (!valid) return res.status(400).send("Firma invalida");

 // 4. Deduplique por ID de webhook
 const deliveryId = req.headers["x-quotery-webhook-id"];
 if (processedIds.has(deliveryId)) {
 return res.sendStatus(200); // Ya procesado, confirmar silenciosamente
 }
 processedIds.add(deliveryId);

 // 5. Procese el evento
 const eventType = req.headers["x-quotery-event"];
 const payload = req.body;
 console.log(`Recibido ${eventType}`, payload);

 switch (eventType) {
 case "quote.accepted":
 // Cree un pedido en su ERP
 break;
 case "quote.closed":
 // Actualice la etapa del negocio en su CRM
 break;
 case "delivery_note.marked_delivered":
 // Reduzca inventario, active factura
 break;
 default:
 console.log(`Tipo de evento no manejado: ${eventType}`);
 }

 res.sendStatus(200);
});

app.listen(process.env.PORT || 3000, () => {
 console.log("Receptor de webhooks escuchando");
});

La verificación de idempotencia en el paso 4 es importante. Quotery reintenta entregas hasta 6 veces, y cada reintento lleva el mismo X-Quotery-Webhook-Id. Sin deduplicación, procesaría el mismo evento varias veces, creando pedidos duplicados, contando cambios de inventario dos veces, etc.