Cobrar en línea con Mercado Pago es fácil en la demo. En producción aparecen los problemas de verdad: webhooks que llegan dos veces, notificaciones que nunca llegan, pagos aprobados que no se reflejan en tu sistema y, peor, alguien que manda notificaciones falsas a tu endpoint.
Estas son las reglas que aplico en cada integración.
1. Nunca confíes en lo que dice el webhook
La notificación solo te dice “algo cambió en el pago X”. No uses su contenido para marcar un pedido como pagado. Toma el ID y consulta el pago real en la API con tu token:
const r = await fetch(`https://api.mercadopago.com/v1/payments/${paymentId}`, {
headers: { Authorization: `Bearer ${process.env.MP_ACCESS_TOKEN}` },
});
const pago = await r.json();
// pago.status: approved | pending | in_process | rejected | refunded | cancelled...
// pago.external_reference: el folio de TU pedido, que mandaste al crear el cobro
Lo que manda es lo que regresa la API, no lo que llegó al webhook.
2. Valida la firma x-signature
Mercado Pago firma sus notificaciones. En la configuración de webhooks de tu aplicación obtienes una clave secreta; cada notificación trae los encabezados x-signature (con ts y v1) y x-request-id. Recalcula la firma y compárala:
import crypto from 'node:crypto';
function firmaValida(req) {
const sig = req.headers['x-signature'] ?? '';
const requestId = req.headers['x-request-id'] ?? '';
const parts = Object.fromEntries(sig.split(',').map((p) => p.trim().split('=')));
const dataId = String(req.query['data.id'] ?? '').toLowerCase();
const manifest = `id:${dataId};request-id:${requestId};ts:${parts.ts};`;
const esperado = crypto.createHmac('sha256', process.env.MP_WEBHOOK_SECRET).update(manifest).digest('hex');
if (!parts.v1 || parts.v1.length !== esperado.length) return false;
return crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(parts.v1));
}
Si la firma no coincide, responde 401 y no hagas nada más. Revisa la documentación vigente de Mercado Pago por si cambia el formato del manifiesto.
3. Responde rápido y procesa después
Mercado Pago reintenta si no recibe un 2xx a tiempo. No hagas el trabajo pesado dentro del webhook:
- Valida la firma.
- Guarda el evento (ID del pago, fecha) en una tabla o cola.
- Responde 200 de inmediato.
- Un proceso aparte consulta el pago y actualiza el pedido.
4. Idempotencia: el mismo pago no se aplica dos veces
Los webhooks llegan repetidos: es normal. Guarda qué pagos ya aplicaste y hazlo de forma atómica:
// Solo el primero que logre insertar el ID aplica el pago
const res = await db.collection('pagos_aplicados').updateOne(
{ paymentId: pago.id },
{ $setOnInsert: { paymentId: pago.id, pedido: pago.external_reference, aplicadoEn: new Date() } },
{ upsert: true },
);
if (res.upsertedCount === 0) return; // ya se había aplicado
await marcarPedidoPagado(pago.external_reference, pago);
Y al crear cobros por API, manda el encabezado X-Idempotency-Key para que un reintento de tu lado no genere dos cobros.
5. Concilia: no todos los avisos llegan
Aunque todo esté bien hecho, alguna notificación se va a perder (tu servidor estaba reiniciando, un timeout, lo que sea). Programa una conciliación periódica:
- Busca pedidos que siguen “pendientes de pago” después de X minutos.
- Consulta sus pagos por referencia:
GET /v1/payments/search?external_reference=FOLIO. - Si hay un pago aprobado, aplícalo (con la misma lógica idempotente).
- Si el cobro venció, libera el apartado o la reservación y avisa al cliente.
6. Checklist antes de salir a producción
- Credenciales de producción separadas de las de prueba.
- Firma de webhooks validada.
- Estado del pago consultado en la API, no tomado del webhook.
- Aplicación idempotente por ID de pago.
- Conciliación programada.
- Reembolsos y contracargos reflejados en tu sistema.
- Nunca guardas datos de tarjeta: los maneja Mercado Pago.
¿Tu tienda o sistema necesita cobrar en línea sin dolores de cabeza? Platiquemos.