Webhooks

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

HeaderContenido
webhook-idIdentificador del evento. Es el mismo en todos los reintentos y reenvíos, así que también es tu clave de deduplicación.
webhook-timestampMomento del envío, en segundos Unix.
webhook-signaturev1,<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 mismo webhook-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 400 a 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.

Siguientes pasos

¿Te resultó útil esta página?

En esta página