© 2026 quotery.io. Todos los derechos reservados
DHDTech.io · Sheridan, Wyoming
Desarrollado por
DHDTech.io
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.
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 auditoría de todo lo que sucede en su empresa. Usted elige los eventos, usted controla el receptor.
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.closed — Se 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.accepted — Se activa cuando un cliente acepta una cotización a través del portal del cliente. Este es el evento principal para iniciar el cumplimiento; cree un pedido en su ERP, notifique a su almacén o genere una factura de venta.quote.cancelled — Se 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.created — Se 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_delivered — Se activa cuando una nota de entrega se marca como entregada. Esta es la confirmación de que los bienes han llegado al cliente; úsela para reducir inventario, cerrar una tarea de cumplimiento o activar una factura.stock_receipt.completed — Se 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.created — Se 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.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.
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.
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.
El encabezado X-Quotery-Signature se ve así:
t=1714789200,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2import 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.
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 usa 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 cuánto de la firma coincidió. Siempre use una comparación de tiempo constante para la verificación HMAC.
Cada entrega POST incluye estos cuatro encabezados. Los usará para verificación de firma, idempotencia y decisiones de enrutamiento.
| Encabezado | Valor |
|---|---|
| Content-Type | application/json |
| X-Quotery-Webhook-Id | UUID (ej. 550e8400-e29b-41d4-a716-446655440000) |
| X-Quotery-Event | Tipo de evento delimitado por puntos (ej. quote.closed) |
| X-Quotery-Signature | t=1714789200,v1=abc123... |
Content-Type — El cuerpo de la solicitud siempre es JSON. Su endpoint puede asumir esto de forma segura y analizarlo en consecuencia.
X-Quotery-Webhook-Id — Un identificador único para este intento de entrega específico. Úselo como clave de idempotencia: si una entrega se reintenta, llevará el mismo ID, por lo que puede deduplicar verificando si ya ha procesado este ID de webhook.
X-Quotery-Event — Le dice exactamente qué evento se activó. Su endpoint puede usar este valor para enrutar diferentes eventos a diferentes manejadores.
X-Quotery-Signature — La firma HMAC-SHA256. Analícela, verifíquela y solo procese el payload si es válida.
Si su endpoint está caído o devuelve un estado no 2xx, Quotery reintenta la entrega automáticamente. No necesita configurar nada; el programa de reintentos está incorporado.
Las entregas fallidas se reintentan hasta 6 veces usando retroceso exponencial. Aquí está la línea de tiempo desde el intento de entrega inicial:
La ventana completa de reintentos abarca aproximadamente 12 horas. Después de 6 intentos fallidos, la entrega se descarta permanentemente.
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 esté caído o rechazando entregas.
Como los reintentos reutilizan el mismo X-Quotery-Webhook-Id, su endpoint puede manejar entregas duplicadas de forma segura. Mantenga una pequeña caché de IDs de webhook procesados recientemente (unas pocas horas son suficientes dada la ventana de reintentos de 12 horas) y omita cualquier entrega cuyo ID ya haya visto.
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, varía según event_type
}
}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.
¿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.
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.
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.