Skip to main content
Cuando un webhook tiene signing: true, cada petición que Omni Z-API envía a tu endpoint lleva una firma HMAC-SHA256. Esta página describe exactamente qué se firma, para que puedas reproducir el cálculo en tu servidor. Vale para los dos tipos de webhook: de canal y de plantilla.

Headers que recibes

  • t — el instante de la firma, en segundos desde la época (UTC).
  • v1 — el HMAC-SHA256 en hexadecimal minúsculo. El prefijo v1= es la versión del esquema y no forma parte de lo que se firma.
El x-idempotency-key no es opcional si validas la firma: es la única fuente de topic, partition y offset, que entran en el cálculo.

Qué se firma

La firma se calcula sobre una cadena canónica de cinco campos separados por \n (byte 0x0A, sin \r, sin salto de línea final):
El cuerpo entra literalmente, byte a byte tal y como se envió: sin reserializar, sin reordenar claves, sin quitar espacios.
Calcular el HMAC solo sobre el cuerpo no funciona. El cuerpo es únicamente el último de los cinco campos.

Si el cuerpo llega comprimido

Las peticiones de 1024 bytes o más se envían con content-encoding: gzip. La firma se calcula sobre los bytes descomprimidos. Si tu framework HTTP ya descomprime el cuerpo automáticamente, usa el cuerpo que te entrega. Si lees el cuerpo crudo, descomprímelo antes de calcular el HMAC: de lo contrario la validación pasa en los payloads pequeños y falla en los grandes, lo que parece intermitencia.

Ejemplo completo

Todos los valores de abajo son consistentes entre sí: con este secret y este cuerpo, el v1 del header es reproducible. Secret devuelto por la API (ejemplo, no es un secret real):
Petición recibida:
Cadena canónica construida a partir de ella — t viene del x-webhook-signature, y topic, partition y offset vienen del x-idempotency-key:
Comprobación en el terminal:

Validando en tu servidor

Lee el cuerpo crudo. Los frameworks que hacen JSON.parse y luego JSON.stringify antes de que firmes cambian los bytes (orden de claves, espacios, escapes) y la firma no cuadra. En Express, usa express.raw({ type: 'application/json' }) en la ruta del webhook.

Protección contra replay

Compara t con la hora actual de tu servidor y rechaza las peticiones fuera de una ventana aceptable: 5 minutos es un valor razonable. Eso solo no basta: usa también el x-idempotency-key para descartar reentregas del mismo evento, que llegan con el mismo valor.

Rotación del secret

El secret se devuelve únicamente en la respuesta de creación o de actualización con signing: true. El GET no lo devuelve.
Cada PATCH con signing: true genera un secret nuevo e invalida el anterior, incluso cuando la firma ya estaba habilitada y tu intención era solo cambiar otro campo en la misma petición. Si no quieres rotar, omite el campo signing.
Después de rotar, valida con el secret de la última respuesta recibida.

Si la firma no cuadra

  1. ¿Estás firmando los cinco campos, y no solo el cuerpo?
  2. ¿El separador es \n puro, sin \r, y no hay salto de línea al final?
  3. ¿El cuerpo es el crudo, sin reserializar por tu framework?
  4. Si vino content-encoding: gzip, ¿lo descomprimiste antes?
  5. ¿topic, partition y offset salieron del x-idempotency-key de esta misma petición?
  6. ¿El secret es el de la última respuesta con signing: true?