Webhooks: cada respuesta, firmada, a tu sistema
Pega una URL en los ajustes del formulario y cada respuesta llega allí como un POST en JSON, firmado con un secreto que solo ves tú. Sirve para Zapier, Make, n8n o tu propio servidor.
Qué llega
{
"event": "response.created",
"delivery": "5f0c2d1e-8a47-4d8e-9c1b-2f6a7e3b9d10",
"form_id": 123,
"form_title": "Cena de equipo",
"slug": "cena-de-equipo-1a2b3c",
"answers": {
"donde": "Sidrería",
"comentario": "Mejor el viernes"
},
"response_id": 456,
"ts": "2026-09-26T19:42:07Z"
}
answers usa los identificadores de las preguntas de tu formulario. event es
response.created (o ping, el del botón de prueba). delivery es único por
entrega: si la misma llega dos veces, procésala una.
Cabeceras
X-AIM-Timestamp: cuándo salió, en segundos Unix.X-AIM-Signature:sha256=seguido del HMAC-SHA256, en hexadecimal, de<timestamp>.<cuerpo>con el secreto de tu formulario.X-AIM-EventyX-AIM-Delivery: lo mismo que en el cuerpo.
Comprueba la firma
Usa el cuerpo tal como llegó (los bytes, antes de leer el JSON) y rechaza lo que tenga más de 5 minutos: así nadie puede repetir un envío que haya capturado.
Python
import hashlib, hmac, time
def firma_valida(secreto, cabeceras, cuerpo):
ts = cabeceras.get("X-AIM-Timestamp", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
esperada = "sha256=" + hmac.new(secreto.encode(), ts.encode() + b"." + cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, cabeceras.get("X-AIM-Signature", ""))
Node.js
const crypto = require("crypto");
function firmaValida(secreto, cabeceras, cuerpo) { // cuerpo: Buffer, tal cual llegó
const ts = cabeceras["x-aim-timestamp"] || "";
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const esperada = "sha256=" + crypto.createHmac("sha256", secreto).update(ts + ".").update(cuerpo).digest("hex");
const a = Buffer.from(esperada), b = Buffer.from(cabeceras["x-aim-signature"] || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Si tu sistema no contesta
Todo lo que no sea una respuesta 2xx en 8 segundos cuenta como fallo, y reintentamos al cabo de 1 minuto, 5 minutos, 30 minutos, 2 horas, 6 horas y 12 horas: casi un día. La cola vive en la base de datos, así que un reinicio del servidor no la pierde. En los ajustes del formulario ves las últimas entregas y puedes enviar una prueba.
Respuestas anónimas y voto secreto
Para que la hora no delate a nadie, las respuestas de un formulario anónimo o de una votación secreta salen una vez al día, pasada la medianoche (UTC), con la fecha y sin la hora, y sin un número de respuesta que diga el orden.
Tus datos, y los nuestros
El cuerpo de cada entrega se guarda solo hasta que se entrega (o, tras el último intento, se da por perdida) y el registro de entregas se borra a los 7 días. No enviamos a direcciones privadas (tu red local, el propio equipo) ni seguimos redirecciones: si tu URL redirige, la entrega cuenta como fallida. Si crees que tu secreto se ha filtrado, cámbialo desde los ajustes del formulario: el anterior deja de valer al momento.