Wompi: cómo generar la firma de integridad y verificar webhooks, con código

Cómo se calcula la firma de integridad del checkout de Wompi y cómo comprobar que un evento realmente viene de Wompi, con funciones en Python y JavaScript probadas contra el ejemplo de su documentación.

En este artículo
En 30 segundos

Con Wompi firmas el pago en tu servidor con un SHA-256 de referencia, monto en centavos, moneda y tu secreto de integridad, y verificas cada evento recalculando un SHA-256 con las propiedades que el evento declara, su timestamp y tu secreto de eventos.

1. Dos firmas distintas, dos secretos distintos#

Integrar Wompi en un sitio implica dos comprobaciones que se confunden con facilidad. La firma de integridad protege el pago que tu servidor le entrega al checkout: evita que alguien cambie el monto en el navegador. La verificación del evento protege tu servidor: comprueba que la notificación de un cambio de estado viene de Wompi y no de un tercero.

Cada una usa un secreto distinto, que no es ni la llave pública ni la privada. Según la documentación de Wompi, ambos están en el panel de comercios, en la sección de secretos para integración técnica, y no deben salir de tu servidor.

Documentación: Wompi: Widget y Checkout Web (firma de integridad) ↗ · Wompi: Eventos (verificación del checksum) ↗

2. La firma de integridad del checkout#

La documentación indica que la firma es el SHA-256, en hexadecimal, de la concatenación de la referencia, el monto en centavos, la moneda y el secreto de integridad. Si el pago tiene fecha de expiración, esa fecha va entre la moneda y el secreto. El monto va en centavos: 95.000 pesos se envían como 9500000, y la única moneda disponible es COP.

DatoValorNota
Referenciask8-438k4-xmxm392-sn2mTu identificador único del pago
Monto2490000$24.900 COP en centavos
MonedaCOPLa única disponible hoy
Secretoprod_integrity_…Nunca en el navegador
Cadenask8-438k4-xmxm392-sn2m2490000COPprod_integrity_…Se le aplica SHA-256 en hexadecimal

La tabla reconstruye el ejemplo de la documentación: una referencia, un monto de 24.900 pesos (2490000 centavos) y el secreto de ejemplo. El código siguiente produce exactamente el hash que la documentación publica para esa cadena.

3. Verificar un evento (webhook)#

Para comprobar un evento, la documentación indica recalcular el checksum así: leer la lista signature.properties del propio evento; buscar cada ruta dentro del campo data y concatenar los valores en ese orden; añadir el timestamp del evento y, al final, el secreto de eventos; aplicar SHA-256 y comparar con el checksum recibido, sin distinguir mayúsculas de minúsculas. Si no coinciden, ignora el evento.

La lista de propiedades puede variar, así que no la escribas fija en tu código: léela del evento, como hacen las funciones de abajo. Las funciones comparan en tiempo constante.

python
import hashlib
import hmac

def firma_integridad(referencia, monto_en_centavos, moneda, secreto, expiracion=""):
    """Firma del checkout: SHA-256 de referencia + monto + moneda [+ expiración] + secreto."""
    cadena = f"{referencia}{monto_en_centavos}{moneda}{expiracion}{secreto}"
    return hashlib.sha256(cadena.encode()).hexdigest()

def valor_en(data, ruta):
    """Lee una ruta como 'transaction.status' dentro del campo data del evento."""
    actual = data
    for parte in ruta.split("."):
        actual = actual[parte]
    return str(actual)

def evento_autentico(evento, secreto_eventos):
    """Recalcula el checksum con las propiedades que declara el propio evento."""
    firma = evento["signature"]
    valores = "".join(valor_en(evento["data"], p) for p in firma["properties"])
    cadena = f"{valores}{evento['timestamp']}{secreto_eventos}"
    esperado = hashlib.sha256(cadena.encode()).hexdigest()
    return hmac.compare_digest(esperado.lower(), firma["checksum"].lower())

# Ejemplo de la documentación de Wompi: $24.900 COP son 2490000 centavos
print(firma_integridad("sk8-438k4-xmxm392-sn2m", 2490000, "COP", "prod_integrity_Z5mMke9x0k8gpErbDqwrJXMqsI6SFli6"))

# Evento de prueba con valores y secreto inventados
secreto = "test_events_secreto_de_ejemplo"
evento = {
    "data": {"transaction": {"id": "T-0001", "status": "APPROVED", "amount_in_cents": 9500000}},
    "timestamp": 1760000000,
    "signature": {"properties": ["transaction.id", "transaction.status", "transaction.amount_in_cents"]},
}
cadena = "T-0001" + "APPROVED" + "9500000" + "1760000000" + secreto
evento["signature"]["checksum"] = hashlib.sha256(cadena.encode()).hexdigest().upper()
print(evento_autentico(evento, secreto), evento_autentico(evento, "otro_secreto"))
Python 3, solo biblioteca estándar. La primera línea es el hash del ejemplo de la documentación; la segunda, un evento de prueba válido y el mismo evento con otro secreto.
javascript
import { createHash, timingSafeEqual } from "node:crypto";

const sha256 = texto => createHash("sha256").update(texto).digest("hex");

function firmaIntegridad(referencia, montoEnCentavos, moneda, secreto, expiracion = "") {
  return sha256(`${referencia}${montoEnCentavos}${moneda}${expiracion}${secreto}`);
}

function valorEn(data, ruta) {
  return String(ruta.split(".").reduce((actual, parte) => actual[parte], data));
}

function eventoAutentico(evento, secretoEventos) {
  const { properties, checksum } = evento.signature;
  const valores = properties.map(p => valorEn(evento.data, p)).join("");
  const esperado = Buffer.from(sha256(`${valores}${evento.timestamp}${secretoEventos}`));
  const recibido = Buffer.from(checksum.toLowerCase());
  return esperado.length === recibido.length && timingSafeEqual(esperado, recibido);
}

console.log(firmaIntegridad("sk8-438k4-xmxm392-sn2m", 2490000, "COP", "prod_integrity_Z5mMke9x0k8gpErbDqwrJXMqsI6SFli6"));

const secreto = "test_events_secreto_de_ejemplo";
const evento = {
  data: { transaction: { id: "T-0001", status: "APPROVED", amount_in_cents: 9500000 } },
  timestamp: 1760000000,
  signature: { properties: ["transaction.id", "transaction.status", "transaction.amount_in_cents"] },
};
evento.signature.checksum = sha256("T-0001" + "APPROVED" + "9500000" + "1760000000" + secreto).toUpperCase();
console.log(eventoAutentico(evento, secreto), eventoAutentico(evento, "otro_secreto"));
JavaScript (Node.js, módulo ES). Imprime el mismo hash y true false.

Un detalle importante: al recalcular el ejemplo de eventos de la documentación con los valores que lista, no obtuvimos el checksum que publica. Por eso el evento de prueba de esta guía usa valores y secreto inventados, y te recomendamos validar tu implementación con eventos reales del entorno de pruebas (sandbox) antes de pasar a producción.

Documentación: Python: hashlib ↗ · Node.js: crypto ↗

4. Qué hacer después de verificar#

  • Responde rápido al webhook y haz el trabajo pesado después, para no agotar el tiempo de espera de quien envía el evento.
  • Trata los eventos como idempotentes: guarda el id de la transacción y no apliques dos veces un mismo estado.
  • Compara el monto y la referencia del evento con tu pedido, no confíes solo en el estado.
  • Considera rechazar eventos con un timestamp muy antiguo; es una medida adicional tuya, no un requisito de la documentación.
  • Registra los eventos descartados sin guardar los secretos.

Para profundizarColas, reintentos e idempotencia: evita procesar dos veces

5. Errores frecuentes#

  • Calcular la firma de integridad en el navegador y exponer el secreto.
  • Enviar el monto en pesos en vez de centavos.
  • Mezclar el secreto de integridad con el de eventos.
  • Escribir la lista de propiedades del evento en el código en lugar de leerla del evento.
  • Comparar el checksum con == en vez de una comparación de tiempo constante.

Para profundizarVariables de entorno y DATABASE_URL en producción: guía completa

6. Mantén tus cifras al día#

La integración se describe como estaba la documentación de Wompi el 18 de octubre de 2026. Pueden cambiar los campos, las monedas disponibles o el algoritmo; revisa la documentación oficial y prueba en sandbox antes de desplegar.

  • Revisa la documentación de Wompi antes de cada cambio de integración.
  • Prueba con transacciones de sandbox aprobadas, rechazadas y anuladas.
  • Rota los secretos si sospechas que se filtraron.

Fuentes y alcance

Documentación consultada el 18 de octubre de 2026. Los ejemplos y criterios de decisión son propuestas editoriales; adáptalos al contrato de tu aplicación y valídalos en tu entorno de pruebas autorizado.

Compara estas opciones

Compara bases de datos

Revisa tarifas, límites, condiciones y fuentes de cada opción.

Abrir comparador
Asesoría

¿Tu caso es distinto?

Cuéntanos tu contexto y las opciones que estás evaluando. Te respondemos con criterios y fuentes, sin suscripciones automáticas.

Solicita asesoría