Skip to content

Avisos (webhooks) ​

Un aviso es una petición que Zenci le envía a tu tienda cuando cambia algo del catálogo (un producto, sus existencias o su precio) o cuando le pasa algo a una venta que registró tu tienda: se facturó, la factura no se pudo emitir, se anuló o se devolvió. Así tu tienda no tiene que consultar la API a cada rato para enterarse.

El aviso dice qué cambió, no cómo quedó: trae solo identificadores. Al recibirlo, consulta la API para leer el estado actual.

Configurar un aviso ​

Los avisos se configuran en el panel, en Administración → Integraciones → Avisos a tu tienda. No se crean por la API.

  1. Haz clic en Nuevo aviso.
  2. En Dirección de tu tienda, escribe la URL que recibirá los avisos.
  3. En Qué avisar, marca los eventos que quieres recibir.
  4. Haz clic en Crear aviso.
Formulario Nuevo aviso con la dirección de la tienda y los eventos a avisar, agrupados en Catálogo y Ventas de la tiendaFormulario Nuevo aviso con la dirección de la tienda y los eventos a avisar, agrupados en Catálogo y Ventas de la tienda
Nuevo aviso: la dirección de tu tienda y qué eventos avisar.

La dirección debe:

  • empezar con https://;
  • ser un dominio público: no se aceptan direcciones IP, localhost, nombres sin punto ni dominios .local o .internal;
  • no llevar usuario ni contraseña.

Cada comercio puede tener hasta 5 avisos. Un aviso empieza a enviar los cambios desde que se crea; los anteriores no se envían.

La clave de firma ​

Al crear el aviso, Zenci muestra una sola vez su clave de firma, que empieza con whsec_. Guárdala en tu tienda: sirve para comprobar que cada aviso viene de Zenci. Si la pierdes o se filtró, usa .

Ventana Clave de firma de los avisos con la clave whsec_ y el botón CopiarVentana Clave de firma de los avisos con la clave whsec_ y el botón Copiar
La clave de firma se muestra una sola vez, al crear el aviso.

Eventos ​

EventoEn el panelCuándo se envíadata
product.updatedProducto actualizadoCambió la ficha de un producto, una de sus variantes, sus opciones o sus imágenesproductId
product.deletedProducto eliminadoSe eliminó un productoproductId
stock.updatedExistencias actualizadasCambiaron las existencias de una variante en una bodegavariantId, sku, warehouseId
price.updatedPrecio actualizadoCambió el precio de una variante en el catálogo o en una lista de preciosvariantId, sku, priceListId
order.invoicedVenta facturadaLa DGI aceptó la factura de una venta de tu tienda. Llega otra vez si la factura se corrige y se emite una nuevaorderId, invoiceId
order.invoice_failedFactura de una venta no emitidaLa factura no se pudo emitir (ver reason abajo)orderId, invoiceId, reason
order.invoice_cancelledFactura de una venta anuladaEl comercio anuló la factura de una venta de tu tiendaorderId, invoiceId
order.returnedVenta devueltaEl comercio registró una devolución de una venta de tu tiendaorderId, returnId
catalog.resync_requiredVolver a leer el catálogoTu tienda debe releer el catálogo completo. Se envía siempre, aunque no lo marquesreason
webhook.testAviso de pruebaAlguien del comercio pidió . Solo responde 2xxsubscriptionId

Los eventos order.* solo se envían por ventas registradas por la API, de cualquier clave del comercio, nunca por las ventas del POS.

Qué hacer con cada uno:

  • product.updated: lee el producto con y actualízalo en tu tienda.
  • product.deleted: deja de vender ese producto.
  • stock.updated: consulta . Si la variante es la base de una presentación enlazada (la caja de 12, por ejemplo), también llega un aviso por la presentación.
  • price.updated: con un priceListId, cambió el precio en esa lista; si no es la de tu clave (en ), puedes descartarlo. Con priceListId: null, vuelve a leer siempre el precio con tu clave: llega cuando cambia el precio del catálogo, cuando se quita un precio de una lista o por otras ediciones de la variante, aunque el precio final no haya cambiado.
  • order.invoiced y order.invoice_cancelled: consulta para leer la factura y su enlace al PDF.
  • order.invoice_failed: según reason. rejected (la DGI la rechazó) y review_required (respuesta ambigua) los resuelve el comercio en Zenci. customer_missing_dv y customer_incomplete dicen que faltan datos del cliente (el DV, o el pasaporte, el país o el correo de un extranjero): corrígelos con y Zenci vuelve a intentar la factura sola en las horas siguientes. merchant_setup es un problema de la configuración de facturación del comercio. En los tres últimos, invoiceId es null.
  • order.returned: la devolución ya se registró en Zenci; si tu tienda lleva sus propias devoluciones, márcala.
  • catalog.resync_required: consulta .

Qué recibe tu tienda ​

Zenci hace un POST a tu dirección con este cuerpo:

json
{
  "id": "e8r2t6y0u4i7o1p5a9s3d7f1",
  "event": "stock.updated",
  "createdAt": "2026-10-06T15:31:04.000Z",
  "data": {
    "variantId": "v3q7w1e5r9t2y6u0i4o8p2a6",
    "sku": "MB-029-CRU-38",
    "warehouseId": "w2e6r0t4y8u1i5o9p3a7s1d5"
  }
}
CampoDescripción
idIdentificador del aviso. Es el mismo en todos sus reintentos
eventEl evento
createdAtCuándo se generó el aviso
dataLos identificadores de lo que cambió, según el evento

Otros ejemplos de data:

json
{ "productId": "p4k8m2x9q7w1e5r3t6y0u2i8" }
json
{ "variantId": "v3q7w1e5r9t2y6u0i4o8p2a6", "sku": "MB-029-CRU-38", "priceListId": null }
json
{ "reason": "change_history_expired" }
json
{ "orderId": "o5h9j3k7l1z4x8c2v6b0n4m8", "invoiceId": null, "reason": "customer_missing_dv" }

Y estos encabezados:

EncabezadoValor
Content-Typeapplication/json
User-AgentZenci-Webhooks/1.0
X-Zenci-EventEl evento, igual que event
X-Zenci-DeliveryEl identificador del aviso, igual que id
X-Zenci-Signaturet=<segundos>,v1=<firma>

Comprobar la firma ​

X-Zenci-Signature tiene dos partes:

  • t: el momento del envío, en segundos desde 1970 (hora Unix).
  • v1: HMAC-SHA256, en hexadecimal, del texto <t>.<cuerpo> con tu clave de firma whsec_… completa.

Para comprobarla:

  1. Toma el cuerpo tal como llegó, sin convertirlo a objeto y volver a texto: un espacio o un orden de campos distinto cambia la firma.
  2. Calcula el HMAC-SHA256 de <t>.<cuerpo> con tu clave y compáralo con v1 en tiempo constante.
  3. Rechaza los avisos con un t muy viejo, por ejemplo de más de 5 minutos, para que nadie pueda reenviarte un aviso capturado. Zenci no aplica esta tolerancia por ti; la decides tú. Mantén sincronizado el reloj de tu servidor.

Ejemplo en Node.js, sin dependencias:

js
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const SECRET = process.env.ZENCI_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;

function isValidSignature(rawBody, header) {
  const parts = Object.fromEntries(
    String(header ?? "")
      .split(",")
      .map((part) => part.trim().split("=")),
  );
  const timestamp = Number(parts.t);
  if (!parts.t || !parts.v1 || !Number.isInteger(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", SECRET)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const received = Buffer.from(parts.v1);
  const computed = Buffer.from(expected);
  return received.length === computed.length && timingSafeEqual(received, computed);
}

createServer((req, res) => {
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const rawBody = Buffer.concat(chunks).toString("utf8");
    if (!isValidSignature(rawBody, req.headers["x-zenci-signature"])) {
      res.writeHead(401).end();
      return;
    }
    res.writeHead(200).end();
    handleEvent(JSON.parse(rawBody));
  });
}).listen(3000);

Si usas un framework, pide el cuerpo crudo: en Express, por ejemplo, con express.raw({ type: "application/json" }) en esa ruta.

Responder al aviso ​

  • Responde con un estado 2xx en menos de 5 segundos. Pasado ese tiempo, Zenci da el intento por fallido.
  • Responde primero y procesa después, como en el ejemplo. Consultar la API puede tomar más que eso.
  • Zenci no sigue redirecciones: un 3xx cuenta como fallo definitivo. Configura la dirección final.

Reintentos ​

Respuesta de tu tiendaQué hace Zenci
2xxLo da por Entregado
408, 429, 5xx, o ninguna respuesta (error de red, tiempo agotado)Lo reintenta más tarde
Cualquier otro 4xx, o un 3xxLo da por Fallido y no lo reintenta

Un aviso se intenta hasta 8 veces. Entre un intento y el siguiente espera cada vez más: 1 minuto, 3, 9, 27, 81 minutos (1 h 21 min), unas 4 horas y 12 horas. Si el octavo intento falla, el aviso queda Fallido, unas 18 horas después del primero.

Ten en cuenta:

  • Un aviso puede llegar más de una vez. Cada reintento lleva el mismo id y un t nuevo. Guarda los id que ya procesaste y descarta los repetidos.
  • El orden no está garantizado. Un reintento puede llegar después de un aviso más nuevo. Por eso conviene consultar la API al recibirlo, en vez de aplicar lo que traiga el aviso.
  • No es instantáneo. Zenci envía los avisos cada minuto, así que un cambio llega en uno o dos minutos.
  • Si a la API del comercio se le desactiva el acceso, o su suscripción queda restringida, los avisos esperan sin gastar intentos y se envían cuando se restablece.

Administrar un aviso ​

En la lista de avisos ves la dirección, los eventos y el estado de cada uno: Activa, Pausada o Requiere resincronización. El menú ⋯ de cada aviso tiene estas acciones:

Lista de avisos con el menú de acciones abierto: Ver entregas, Enviar aviso de prueba, Cambiar clave de firma, Pausar y EliminarLista de avisos con el menú de acciones abierto: Ver entregas, Enviar aviso de prueba, Cambiar clave de firma, Pausar y Eliminar
Acciones de un aviso: ver entregas, enviar un aviso de prueba, cambiar la clave de firma, pausar y eliminar.
  • Ver entregas muestra los últimos 50 avisos enviados a esa dirección, del más nuevo al más viejo, con su evento, estado (Pendiente, Entregado o Fallido), intentos, la última respuesta de tu tienda y la fecha. Es el primer lugar donde mirar si tu tienda no recibe avisos. Un aviso Fallido tiene el botón Reintentar, que lo vuelve a la cola con sus 8 intentos desde cero: úsalo cuando ya corregiste lo que hacía fallar a tu tienda.
  • Enviar aviso de prueba y Cambiar clave de firma se explican abajo.
  • Pausar deja de enviar avisos a esa dirección. Los cambios no se pierden: al Activar de nuevo, se envían los que ocurrieron mientras estuvo pausado.
  • Eliminar detiene los avisos de inmediato. Los que estaban pendientes ya no se envían.
Ventana Entregas recientes con el evento, el estado, los intentos y la última respuesta de cada avisoVentana Entregas recientes con el evento, el estado, los intentos y la última respuesta de cada aviso
Entregas recientes: avisos de ventas y del catálogo, uno pendiente tras un 503 y uno fallido por un 404, con su botón Reintentar.
Página Integraciones con dos claves de API y dos avisos, uno pausado y otro activoPágina Integraciones con dos claves de API y dos avisos, uno pausado y otro activo
Integraciones con claves de API y avisos configurados.

Enviar un aviso de prueba ​

Enviar aviso de prueba manda un webhook.test a la dirección por el mismo camino que un aviso real: firmado, con sus reintentos y visible en Ver entregas. Sirve para comprobar que tu tienda recibe los avisos y valida la firma antes de esperar un cambio real. Sale en menos de un minuto. Un aviso pausado no se puede probar.

Cambiar la clave de firma ​

Cambiar clave de firma genera una clave whsec_ nueva y la muestra una sola vez. La anterior deja de firmar en ese momento, también en los reintentos de avisos pendientes: hasta que copies la nueva en tu tienda, tu tienda va a rechazar los avisos y Zenci los va a reintentar. Hazlo cuando la clave se haya perdido o filtrado.

Resincronizar ​

Zenci guarda el historial de cambios durante 30 días. Si un aviso queda pausado o sin enviar más tiempo que eso, ya no puede saber todo lo que cambió. Entonces:

  1. Envía catalog.resync_required, aunque no lo hayas marcado en Qué avisar.
  2. El aviso pasa a Requiere resincronización y deja de enviar cambios.

Para volver a la normalidad:

  1. Tu tienda relee el catálogo completo con , sin updatedSince, y las existencias con GET /stock.
  2. Cuando termine, alguien del comercio hace clic en Reanudar en ese aviso, en el panel.

Al reanudar, Zenci envía los cambios ocurridos desde que pidió la resincronización. Un cambio hecho mientras tu tienda releía puede llegar repetido, pero no se pierde.

Siguientes pasos ​

  • — Lo que tu tienda consulta al recibir un aviso.
  • — Formato de los datos y errores.

Documentación oficial de Zelta