Webhooks firmados
CDE firma lo que envía con HMAC-SHA256. Son dos formatos y no son intercambiables: la publicación en sitio propio firma la hora junto con el cuerpo, en las cabeceras x-cde-timestamp y x-cde-signature; los webhooks de evento firman solo el cuerpo, en la cabecera cde-signature.
La diferencia existe por un motivo: la publicación tiene que resistir el reenvío de una petición capturada, así que la hora entra en la firma y el destino rechaza lo que esté fuera de la ventana. El webhook de evento se entrega con registro y puedes reenviarlo a propósito — ahí la hora en la firma estorbaría.
Publicación en sitio propio
- x-cde-timestamp: la hora del envío, en segundos desde 1970.
- x-cde-signature: sha256=<hexadecimal> del HMAC sobre la cadena `${timestamp}.${cuerpo}`.
- El destino recalcula la misma cadena a partir del cuerpo EN BRUTO y la cabecera de hora.
- Rechaza lo que esté fuera de la ventana que consideres segura (la plantilla de CDE usa cinco minutos).
Webhooks de evento
- cde-event: el nombre del evento que registraste en el panel.
- cde-signature: sha256=<hexadecimal> del HMAC sobre el cuerpo, sin hora.
- cde-delivery-id: el identificador de la entrega, para descartar repeticiones.
- cde-endpoint y cde-version: qué endpoint la recibió y la versión del formato.
Verificar la firma
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, rawBody, signature) {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}PHP
function verify(string $secret, string $rawBody, ?string $signature): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return is_string($signature) && hash_equals($expected, $signature);
}Python
import hmac, hashlib
def verify(secret: str, raw_body: bytes, signature: str) -> bool:
expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or '')- Usa siempre el cuerpo EN BRUTO de la petición. Si tu servidor ya convirtió el JSON en objeto y lo vuelves a serializar, la firma no coincidirá.
- Compara con una función de tiempo constante (timingSafeEqual, hash_equals, compare_digest), nunca con igualdad simple.
- Guarda el identificador de la entrega y descarta repeticiones: el reenvío es una función, no un error.
¿No encontraste lo que necesitas?
La documentación sigue al producto y crece con las preguntas que llegan. Escríbenos por el formulario de contacto diciendo qué faltó.