api.denorly.com
Ir al sitio →
Trata sobre: Formularios Libro de Reclamaciones

Webhooks

En cada envío, Denorly hace POST a tu URL con el payload firmado. Verifica la firma antes de confiar en el body.

Algoritmo
HMAC-SHA256 (hex)
Firma sobre
timestamp + "." + body
Secret
64 hex chars (dashboard)

Headers entrantes

Header Valor
X-Denorly-Signature HMAC-SHA256 en hex de "{timestamp}.{rawBody}".
X-Denorly-Timestamp Epoch en segundos (string). Es el mismo valor usado al firmar.

Construcción de la firma

Denorly calcula exactamente esto. Tú lo recalculas con el mismo raw body y comparas.

signed_payload = timestamp + "." + rawRequestBody
signature      = HMAC_SHA256(signing_secret, signed_payload)   // → hex

Verificación

import crypto from "node:crypto";
import express from "express";

const app = express();
const SIGNING_SECRET = process.env.DENORLY_SIGNING_SECRET;

// IMPORTANTE: necesitas el RAW body, no el JSON ya parseado.
app.post("/webhooks/denorly", express.raw({ type: "*/*" }), (req, res) => {
  const signature = req.header("X-Denorly-Signature");
  const timestamp = req.header("X-Denorly-Timestamp");
  const rawBody   = req.body; // Buffer

  const signedPayload = `${timestamp}.${rawBody.toString("utf8")}`;
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(signedPayload)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send("invalid signature");
  }

  const event = JSON.parse(rawBody.toString("utf8"));
  // ... procesa event
  res.sendStatus(200);
});

⚠ Gotcha: esto rompe en producción:

  • Firma sobre el RAW body exacto que recibiste. Si tu framework parsea el JSON y lo re-serializas, los bytes cambian (espacios, orden de claves) y la firma nunca coincide.
  • Es hex, no base64. .digest("hex") / hexdigest() / hash_hmac(..., false).
  • Compara con timing-safe (timingSafeEqual, hash_equals, compare_digest), no con ==.
  • El payload firmado lleva el timestamp y el punto literal: "{timestamp}.{rawBody}". Sin el prefijo no valida.

Payload por defecto

Sin plantilla configurada, el body es el JSON con los campos enviados más tres campos meta. Las claves de los campos son las que tú enviaste al formulario.

{
  "nombre": "Andrés",
  "email": "andres@studio.pe",
  "mensaje": "Hola",
  "submission_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "form_id": "8f3b2c1a-9d4e-4f7a-b6c2-1e5a7d9c0b3f",
  "submitted_at": "2026-06-11T14:32:08Z"
}

Si configuras una plantilla en el dashboard, el shape lo defines tú con variables {{submission.id}}, {{form.name}}, {{campo}}, etc. El Content-Type del POST es el que configures en el webhook (por defecto application/json).

Reintentos

Intento Espera tras el fallo
11 min
25 min
315 min
41 hora
52 horas

Hasta 5 reintentos. Timeout por intento: 30 s. Cuenta como éxito cualquier respuesta 2xx; cualquier otra cosa (o timeout/conexión fallida) dispara reintento. Responde rápido y encola el procesamiento pesado.