Verifica la firma
Comprueba que cada webhook viene de Tonder: los headers de Standard Webhooks, cómo se calcula la firma y cómo rotar el secreto.
Cada webhook de Webhooks 2.0 viene firmado con el estándar
Standard Webhooks, así que sus librerías oficiales
(standardwebhooks) y las de Svix lo verifican tal cual. Verifica la firma antes de procesar un
evento y rechaza cualquier webhook que no la pase.
Webhooks 2.0 se habilita por cuenta. Si en Desarrolladores de tu dashboard no aparece la pestaña Eventos, tu cuenta está en la versión anterior: pide a tu contacto de Tonder que la habilite.
Sin Webhooks 2.0, los webhooks no vienen firmados: apóyate en el
header de autenticación y confirma el estado con
GET /api/v1/transactions/{id}/.
Los headers
| Header | Contenido |
|---|---|
webhook-id | Identificador del evento. Es el mismo en todos los reintentos y reenvíos, así que también es tu clave de deduplicación. |
webhook-timestamp | Momento del envío, en segundos Unix. |
webhook-signature | v1,<firma en base64>. Durante una rotación del secreto llegan dos firmas, separadas por un espacio. |
Cómo se calcula la firma
La firma es el HMAC-SHA256 de <webhook-id>.<webhook-timestamp>.<cuerpo exacto>. La llave es tu
secreto de firma sin el prefijo whsec_, decodificado de base64. Un webhook es válido si la
firma que calculas coincide con cualquiera de las que trae webhook-signature.
Verifica con la librería oficial (Node.js)
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.TONDER_WEBHOOK_SECRET); // "whsec_..."
app.post("/webhooks/tonder", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = wh.verify(req.body, req.headers); // lanza un error si la firma no es válida
queueForProcessing(event); // procesa después de responder
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
});La librería también rechaza un webhook cuyo timestamp se aleje más de cinco minutos.
Verifica sin librería (Python)
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes) -> bool:
h = {k.lower(): v for k, v in headers.items()} # los nombres de header no distinguen mayúsculas
try:
msg_id = h["webhook-id"]
timestamp = h["webhook-timestamp"]
sent_at = int(timestamp)
signatures = h["webhook-signature"].split()
except (KeyError, ValueError):
return False # headers ausentes o mal formados: rechaza
if abs(time.time() - sent_at) > 300: # rechaza mensajes de más de 5 min
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
received = [s[3:] for s in signatures if s.startswith("v1,")] # solo cuentan las firmas v1
return any(hmac.compare_digest(expected, sig) for sig in received)body es el cuerpo crudo de la petición: request.get_data() en Flask, request.body en Django.
Reglas para que la verificación funcione
- Usa el cuerpo exacto que recibiste. No parsees y vuelvas a serializar el JSON antes: cualquier
cambio de espacios invalida la firma. En Express eso significa
express.raw, como arriba. - Rechaza timestamps viejos. El ejemplo en Python rechaza mensajes de más de cinco minutos, lo que bloquea solicitudes repetidas por un tercero. Mantén sincronizado el reloj de tu servidor (NTP): la ventana depende de él.
- Deduplica con
webhook-id. Un reintento o un reenvío manual trae el mismowebhook-id; guarda los que ya procesaste durante al menos 60 días —un evento se puede reenviar mientras se conserva— e ignora los repetidos. - Rechaza sin dar detalles. Responde
400a un webhook que no pasa la verificación, no digas qué revisión falló y no lo proceses. - Guarda el secreto en tu servidor. Ponlo en una variable de entorno o en un gestor de secretos —nunca en código del frontend o de una app móvil, en tu repositorio ni en tus logs. Si queda expuesto, rótalo.
Rota el secreto
En Desarrolladores › Webhooks, abre el menú de acciones del endpoint y elige Rotar secreto.
Se genera un secreto nuevo, que se muestra una sola vez. El anterior sigue firmando durante
24 horas, así que tienes ese tiempo para actualizar tu servidor sin perder eventos —mientras
tanto, webhook-signature trae las dos firmas, y la regla de coincidir con cualquiera acepta
ambas.
Si el secreto quedó expuesto, no esperes las 24 horas: cambia tu servidor al secreto nuevo de inmediato y elimina el anterior. Tonder sigue firmando con los dos, pero tu servidor solo acepta los secretos que tiene, así que un webhook falsificado con el secreto anterior falla en cuanto lo eliminas.
