DEV Community

Javier Sivack
Javier Sivack

Posted on Edited on

Webhooks de Mercado Pago que no pierden pagos (ni regalan accesos)

Para devs que ya integraron Checkout Pro y quieren dormir tranquilos.

Integrar el botón de pago de Mercado Pago lleva una tarde. Los problemas aparecen después,
en producción: un pago aprobado que nunca liberó el producto, un cliente al que le llegaron
dos emails de bienvenida, una orden que volvió a "pendiente" sola. Casi siempre la causa está
en el endpoint que recibe los webhooks.

Estos son los seis errores que más veo y cómo evitarlos.

1. No verificar la firma

Tu endpoint de webhooks es público: cualquiera puede mandarle un POST que diga "el pago 123
está aprobado". Mercado Pago firma cada notificación con el header x-signature:

x-signature: ts=1704908010,v1=618c85345248dd820d5fd456117c2ab2...
x-request-id: 3b6d3d0e-...
Enter fullscreen mode Exit fullscreen mode

v1 es un HMAC-SHA256, calculado con la clave secreta de tu aplicación, de este texto:

id:<data.id>;request-id:<x-request-id>;ts:<ts>;
Enter fullscreen mode Exit fullscreen mode

Tres detalles que suelen fallar:

  • data.id sale de la query string (?data.id=123&type=payment) y va en minúsculas.
  • Si falta alguno de los valores, esa parte se omite entera (incluido su ;).
  • Compará con una función de tiempo constante (crypto.timingSafeEqual), no con ===.
const manifest = `id:${dataId.toLowerCase()};request-id:${requestId};ts:${ts};`;
const expected = createHmac("sha256", secret).update(manifest).digest("hex");
const ok = timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(v1, "hex"));
Enter fullscreen mode Exit fullscreen mode

Si la firma no coincide, respondé 401.

2. Confiar en el cuerpo del webhook

Aun con la firma verificada, el cuerpo solo te dice qué cambió, no cómo quedó. Tomá el
data.id y consultá el pago:

GET https://api.mercadopago.com/v1/payments/{id}
Enter fullscreen mode Exit fullscreen mode

Esa respuesta es la fuente de verdad: estado, monto, moneda y external_reference. Como
efecto secundario, un webhook repetido o viejo se vuelve inofensivo, porque siempre leés el
estado actual.

3. Confiar en la URL de retorno

Cuando el comprador vuelve a tu sitio, Mercado Pago agrega parámetros como
?status=approved&payment_id=.... Cualquiera puede escribir esa URL a mano. La página de
retorno tiene que preguntarle el estado a tu servidor (que lo actualizó el webhook) y
mostrar "esperando confirmación" mientras siga pendiente.

4. Disparar efectos por cada notificación

Mercado Pago reintenta si no respondés a tiempo (tiene un timeout de 22 segundos), y a veces
manda dos notificaciones casi juntas. Si tu código hace "llegó un webhook de pago aprobado →
mandar email", vas a mandar emails duplicados.

La regla es disparar efectos por cambio de estado, no por notificación. En la base de
datos se hace con un compare-and-set:

UPDATE orders SET status = 'paid', payment_id = $2
WHERE id = $1 AND status = 'pending';
-- rowCount = 1 → este proceso hizo el cambio: dispará el efecto
-- rowCount = 0 → otro webhook ya lo hizo: no hagas nada
Enter fullscreen mode Exit fullscreen mode

5. Aceptar cualquier transición

Las notificaciones no llegan necesariamente en orden. Si procesás un pending que llega
después del approved, la orden vuelve a pendiente y le cortás el acceso a alguien que pagó.
Definí qué transiciones son válidas y descartá las demás:

Desde Puede pasar a
pending paid, failed, in_dispute
failed paid (el comprador reintentó con otra tarjeta)
paid in_dispute, refunded, charged_back
refunded, charged_back —

6. No validar el monto

Que el pago esté aprobado no significa que sea el pago que esperabas. Pasa más de lo que
parece: el precio llegó desde el navegador, cambiaste el precio y quedó una preferencia vieja
dando vueltas, o un link de pago se reutilizó para otra orden. A mí me pasó probando: la orden
era de 1.500 pesos argentinos y la cuenta de prueba era de Uruguay, así que el pago aprobado
llegó por 1.500 pesos uruguayos. Antes de marcar la orden como
pagada, compará transaction_amount y currency_id con lo que esperabas. Si no coinciden,
dejá la orden en revisión en vez de dar acceso.

Bonus: qué responder

Situación Respuesta Por qué
Firma inválida 401 No es de Mercado Pago
Pago inexistente (404 de la API) 200 La "notificación de prueba" del panel usa un id inventado; reintentar no sirve
La API falla o tu token es inválido 500 Mercado Pago reintenta y no perdés la notificación
Procesado (cambie o no el estado) 200 Listo

Ojo con el último caso: si tu access token está mal configurado y respondés 200, perdés
todas las notificaciones sin enterarte.


Armé todo esto en PagoKit, un starter de Next.js + TypeScript con tests para cada caso:
https://github.com/javiersivack/pagokit (gratis, MIT). La versión Pro suma suscripciones,
reembolsos, Postgres y un outbox con reintentos para que los efectos tampoco se pierdan:
https://pagokit-site.vercel.app


¿Preferís no integrarlo vos? Hago integraciones con Mercado Pago y otras APIs para equipos y agencias: https://javiersivack.com/es#contact

Top comments (0)