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-...
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>;
Tres detalles que suelen fallar:
-
data.idsale 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"));
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}
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
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)