Verificar firmas de webhook
Updated February 28, 2026
Also available in:
A continuación te explicamos cómo verificar las firmas de webhook de Formspree en el código de tu receptor.
El detalle clave es que Formspree no firma solo el cuerpo en bruto. Firma tanto la marca de tiempo como el cuerpo en bruto, en este formato:
{timestamp}.{raw_body}
La firma se envía en el encabezado Formspree-Signature usando este formato:
t=<unix>,v1=<hex>
Requisitos clave
- Usa el cuerpo de la solicitud en bruto exactamente como se recibió.
- Analiza el encabezado
Formspree-Signaturepara extraertyv1. - Calcula
HMAC_SHA256(secret, f"{t}.{raw_body}")y compáralo conv1. - Opcional: rechaza solicitudes más antiguas que una ventana de tolerancia (protección contra replay).
Ejemplos de código
Aquí tienes algunos fragmentos de código para ayudarte a empezar.
Python (Flask)
import hmac
import hashlib
import time
from flask import Flask, request, abort
app = Flask(__name__)
SIGNING_SECRET = "your_signing_secret" # Set this to the secret shown in Formspree
def verify_formspree_signature(secret, signature_header, raw_body, tolerance=300):
if not signature_header:
return False
try:
# Header format: "t=<unix>,v1=<hex>"
parts = dict(item.split("=", 1) for item in signature_header.split(","))
timestamp = int(parts.get("t", "0"))
received_sig = parts.get("v1")
except Exception:
return False
if not received_sig:
return False
# Optional: reject old requests to prevent replay attacks
if abs(int(time.time()) - timestamp) > tolerance:
return False
# Formspree signs: "{timestamp}.{raw_body}"
signed_payload = f"{timestamp}.{raw_body}"
# Compute HMAC-SHA256 with the shared secret
expected_sig = hmac.new(
key=secret.encode("utf-8"),
msg=signed_payload.encode("utf-8"),
digestmod=hashlib.sha256,
).hexdigest()
# Constant-time compare to avoid timing attacks
return hmac.compare_digest(expected_sig, received_sig)
@app.post("/webhook")
def webhook():
# Important: read the raw request body as sent
raw_body = request.get_data(as_text=True)
signature = request.headers.get("Formspree-Signature", "")
if not verify_formspree_signature(SIGNING_SECRET, signature, raw_body):
abort(401)
return ("ok", 200)
JavaScript (Node.js + Express)
const crypto = require("crypto");
const express = require("express");
const app = express();
const SIGNING_SECRET = "your_signing_secret"; // Set this to the secret shown in Formspree
// Capture the raw body before JSON parsing changes it
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString("utf8");
}
}));
function verifyFormspreeSignature(secret, signatureHeader, rawBody, tolerance = 300) {
if (!signatureHeader) return false;
// Header format: "t=<unix>,v1=<hex>"
const parts = Object.fromEntries(
signatureHeader.split(",").map(s => s.split("=", 2))
);
const timestamp = parseInt(parts.t || "0", 10);
const receivedSig = parts.v1;
if (!receivedSig) return false;
// Optional: reject old requests to prevent replay attacks
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > tolerance) return false;
// Formspree signs: "{timestamp}.{raw_body}"
const signedPayload = `${timestamp}.${rawBody}`;
// Compute HMAC-SHA256 with the shared secret
const expectedSig = crypto
.createHmac("sha256", secret)
.update(signedPayload, "utf8")
.digest("hex");
// Constant-time compare to avoid timing attacks
return crypto.timingSafeEqual(
Buffer.from(expectedSig, "utf8"),
Buffer.from(receivedSig, "utf8")
);
}
app.post("/webhook", (req, res) => {
const signature = req.header("Formspree-Signature") || "";
if (!verifyFormspreeSignature(SIGNING_SECRET, signature, req.rawBody)) {
return res.status(401).send("invalid signature");
}
res.status(200).send("ok");
});
app.listen(3000);
Resolución de problemas y problemas comunes
Aquí tienes algunos errores comunes a tener en cuenta:
- Re-serializar JSON antes de firmar: Verifica contra los bytes del cuerpo de la solicitud en bruto exactamente como se recibieron, ya que analizar y volver a serializar JSON cambiará la secuencia de bytes y romperá el HMAC.
- Firmar solo el cuerpo (sin el prefijo
t.): Como se mencionó arriba, Formspree firma la marca de tiempo y la carga útil juntas, así que debes calcular el HMAC sobret.<raw_body>(incluyendo el delimitadort.) en lugar de solo el cuerpo. - Comparar contra toda la cadena del encabezado en lugar del valor
v1: Extrae el valor de la firmav1del encabezado de la firma y compara solo ese hex digest, no el contenido completo del encabezado separado por comas. - Usar un secret diferente al
signing_secretconfigurado del webhook: Asegúrate de que el secret que usas para HMAC sea exactamente el secret de firma del webhook desde la configuración del webhook de Formspree, no tu API key u otro secret del proyecto.