Webhooks
En cada envío, Denorly hace POST a tu URL con el payload firmado. Verifica la firma antes de confiar en el body.
HMAC-SHA256 (hex)
timestamp + "." + body
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);
});
<?php
$secret = getenv('DENORLY_SIGNING_SECRET');
$signature = $_SERVER['HTTP_X_DENORLY_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_DENORLY_TIMESTAMP'] ?? '';
// RAW body: nunca json_decode antes de verificar.
$rawBody = file_get_contents('php://input');
$signedPayload = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $signedPayload, $secret); // hex
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('invalid signature');
}
$event = json_decode($rawBody, true);
// ... procesa $event
http_response_code(200);
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["DENORLY_SIGNING_SECRET"].encode()
@app.post("/webhooks/denorly")
def denorly_webhook():
signature = request.headers.get("X-Denorly-Signature", "")
timestamp = request.headers.get("X-Denorly-Timestamp", "")
# RAW body en bytes: no uses request.json antes de verificar.
raw_body = request.get_data()
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(SECRET, signed_payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, "invalid signature")
event = request.get_json()
# ... procesa event
return "", 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
timestampy 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 |
|---|---|
| 1 | 1 min |
| 2 | 5 min |
| 3 | 15 min |
| 4 | 1 hora |
| 5 | 2 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.