
Los webhooks de Mercado Pago Point no llegan: las 3 causas y cómo depurarlas (Node, 2026)
Casi siempre es una de tres: la terminal no está en modo PDV (sin orden no hay webhook), tu endpoint no devolvió HTTP 200 o 201 en 22 segundos (MP la marca como no entregada y reintenta cada 15 minutos), o armas mal el x-signature (funciona en test, falla en prod). Los webhooks son la fuente de verdad, no el polling.
¿Por qué no llegan las notificaciones de Mercado Pago Point? (las 3 causas reales)
Casi siempre es una de tres causas. La terminal no está en modo PDV, así que ninguna orden llega al dispositivo y sin orden no existe webhook. O tu endpoint no devolvió HTTP 200 o 201 en 22 segundos, así que Mercado Pago reintenta cada 15 minutos. O armaste mal el x-signature, que funciona en test y truena en producción. El webhook es la fuente de verdad, no el polling.
Lo digo después de cablear Point en producción con dinero real moviéndose en México.
Vamos por cada causa con el código que la resuelve. Si vienes de armar el cobro, el flujo completo de creación de orden está en cobrar con Mercado Pago Point (Orders API). La referencia oficial es el overview de MP Point.
Los webhooks son la fuente de verdad, no el polling: deja de pegarle al GET de la orden
El error mental número uno con Point es tratarlo como una API síncrona. Creas la orden con POST /v1/orders, y como no te llega el resultado en la respuesta, empiezas a hacer polling contra el GET de la orden cada segundo esperando ver processed. Eso genera rate limiting, latencia y bugs de carrera cuando el cliente tarda en insertar la tarjeta.
El modelo correcto es asíncrono por diseño. Tu servidor crea la orden, la terminal la carga sola, el comprador paga, y Mercado Pago te avisa el resultado por un POST HTTPS a tu webhook. Ese POST trae "type": "order" y un evento como order.processed. Ese evento —no tu polling— dispara tu lógica: imprimir ticket, liberar el producto, encolar la factura.
// MAL: polling contra el GET de la orden esperando el resultado
async function esperarPago(orderId) {
while (true) {
const r = await fetch(`https://api.mercadopago.com/v1/orders/${orderId}`, {
headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },
});
const order = await r.json();
if (order.status === "processed") return order; // frágil, caro, con carreras
}
}
El polling al GET solo sirve como respaldo puntual, por ejemplo para reconciliar una orden que crees perdida. El resultado en tiempo real siempre viene por el webhook. Por qué prefiero eventos a polling lo desarmo en webhooks vs polling vs API.
Point es asíncrono por diseño
Webhook (correcto)
- Creas la orden y respondes al cliente
- MP hace POST a tu endpoint cuando hay resultado
- type: order, evento order.processed
- Cero polling, cero rate limiting
Polling al GET (frágil)
- Golpeas GET /v1/orders/{id} en loop
- Latencia y rate limiting
- Carreras si el cliente tarda en pagar
- Se rompe cuando escalas de terminal
La regla de los 22 segundos: devuelve 200 o 201 y procesa aparte (patrón fast-ack)
Aquí está la causa que revienta silenciosamente en producción. Tu endpoint de webhook debe devolver HTTP 200 o 201 en menos de 22 segundos. Si no, Mercado Pago considera que no entregó la notificación y arranca reintentos.
El error clásico: dentro del handler mandas el correo, generas el CFDI, escribes en tres tablas y luego respondes. Todo eso tarda, a veces más de 22 segundos, y MP marca la notificación como fallida aunque tú sí la procesaste. Resultado: reintentos infinitos y trabajo duplicado.
La solución es el patrón fast-ack: valida la firma, encola el trabajo de forma duradera, responde 200 de inmediato, y deja que un worker haga lo pesado.
import express from "express";
const app = express();
// Necesitamos el body crudo para validar la firma (igual que en Stripe)
app.post(
"/webhooks/mp-point",
express.raw({ type: "application/json" }),
async (req, res) => {
// 1. Valida x-signature ANTES de confiar en nada (ver más abajo)
if (!firmaValida(req)) return res.status(401).send("bad signature");
const evento = JSON.parse(req.body.toString("utf8"));
// 2. Encola de forma duradera usando el order id como clave única
await encolarEvento(evento); // dedupe por data.id, ver idempotencia
// 3. ACK inmediato: 200/201 en < 22s. El worker hace lo pesado.
res.status(200).send("ok");
}
);
La regla es simple: nada de I/O lento antes del res.status(200). El CFDI, el correo y el ticket van en el worker, no en el handler. El patrón fast-ack no es opcional cuando hay una ventana de timeout de por medio.
El reintento cada 15 minutos: qué hace MP cuando tu endpoint falla
Cuando tu endpoint no devuelve 200 o 201 en 22 segundos —timeout, 500, 401, o estaba caído— Mercado Pago no descarta el evento: lo reintenta cada 15 minutos. Después del tercer intento el intervalo se estira, pero los reintentos continúan.
Esto es red de seguridad y fuente de bugs a la vez. Red de seguridad porque si tu servicio estuvo caído 40 minutos, cuando vuelva recibirás las notificaciones pendientes sin perder ninguna venta. Fuente de bugs porque cada reintento entrega otra vez el mismo evento, y si tu handler no es idempotente, imprimes dos tickets o generas dos facturas por un solo cobro.
Dos consecuencias prácticas que tienes que interiorizar:
- Si ves reintentos cada 15 minutos en tus logs, tu endpoint no está devolviendo
200/201a tiempo. No es capricho de MP; es la señal de que tu ACK falla. Revisa timeouts y el status code real que sale. - La entrega es al menos una vez (at-least-once). El mismo evento puede llegar varias veces por diseño, incluso sin fallas tuyas. Tu lógica tiene que sobrevivir duplicados.
Qué hace MP cuando tu endpoint no responde a tiempo
- Envía la notificaciónPOST HTTPS con type: order a tu webhook
- Espera 200 o 201 en 22ssi no llega, la marca como no entregada
- Reintenta cada 15 minutostras el 3er intento el intervalo se estira, pero sigue
- Entrega al menos una vezel mismo evento puede llegar duplicado: deduplica
Qué eventos recibes de verdad (type: order) y qué trae el payload
Antes de depurar conviene saber qué esperas recibir. Todos los eventos de Point llegan con "type": "order". El campo action te dice cuál del ciclo de vida ocurrió:
order.processed— el cobro fue aprobado. Este es el que libera el producto.order.action_required— la terminal o el cliente deben confirmar algo.order.failed— el cobro fue rechazado.order.expired— la orden expiró sin pagarse (porexpiration_time; defaultPT15M).order.canceled— la orden se canceló.order.refunded— se procesó un reembolso.
El payload del POST incluye action, api_version, application_id, data (con el id de la orden, status, total_paid_amount y transactions), date_created, live_mode, type y user_id. Para tu contabilidad, lo confiable es leer del data los campos que te importan.
{
"action": "order.processed",
"api_version": "v1",
"type": "order",
"live_mode": true,
"date_created": "2026-07-06T18:22:05.000Z",
"user_id": "123456789",
"data": {
"id": "ORD01ABCDEF2345GHIJK",
"status": "processed",
"total_paid_amount": "150.00",
"transactions": { "payments": [ { "amount": "150.00" } ] }
}
}
El id de la orden trae prefijo ORD y el total_paid_amount es un string con dos decimales, igual que el amount que mandaste al crearla. El ciclo de vida completo (created → at_terminal → processed/failed/action_required, más expired, canceled y refunded) está en el procesamiento de pagos de MP Point.
Causa #1 en prod: el x-signature mal armado (ts=,v1= + x-request-id + data.id → HMAC-SHA256)
Este es el bug que funciona con las credenciales de test y falla en producción. Mercado Pago firma cada webhook: manda un header x-signature con forma ts=<ms>,v1=<hex> y además un header x-request-id. Tu trabajo es reconstruir el mismo manifest y calcular el HMAC-SHA256; si tu v1 no coincide con el de ellos, la notificación es falsa (o la estás validando mal).
El manifest se arma con tres piezas: el data.id (que viene como query param, en minúsculas), el x-request-id, y el ts del header x-signature. Luego HMAC-SHA256 con el secret del webhook de tu aplicación —se genera por aplicación después de que configuras la URL del webhook y los eventos— y comparas contra v1.
import crypto from "crypto";
function firmaValida(req) {
const xSignature = req.headers["x-signature"]; // "ts=...,v1=..."
const xRequestId = req.headers["x-request-id"];
// 1. Parsea ts y v1 del header x-signature
const parts = Object.fromEntries(
xSignature.split(",").map((kv) => kv.split("=").map((s) => s.trim()))
);
const ts = parts.ts;
const v1 = parts.v1;
// 2. data.id viene como query param; MP lo usa en minúsculas
const dataId = String(req.query["data.id"] || "").toLowerCase();
// 3. Arma el manifest EXACTO: data.id + x-request-id + ts
const manifest = `id:${dataId};request-id:${xRequestId};ts:${ts};`;
// 4. HMAC-SHA256 con el secret del webhook de TU aplicación
const hmac = crypto
.createHmac("sha256", process.env.MP_WEBHOOK_SECRET)
.update(manifest)
.digest("hex");
// 5. Compara contra v1 en tiempo constante
return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(v1));
}
Por qué funciona en test y falla en prod: en test saltas la validación “porque ya jala”; en prod, con el secret real, cualquier byte fuera de lugar en el manifest —un data.id sin pasar a minúsculas, un x-request-id que no incluiste, o un secret cruzado entre aplicaciones— rompe el HMAC. Los SDKs oficiales implementan este check, así que apóyate en ellos si puedes. Si armas la firma a mano, respeta el orden y el formato del manifest al pie de la letra.
Si vienes de Stripe, el concepto es idéntico pero el manifest cambia; el contraste con t=/v1= de Stripe lo detallo en ¿falló la verificación de firma del webhook de Stripe?.
La trampa del modo PDV: sin orden en la terminal no existe webhook
A veces “no llega nada” no es un problema de webhook: es que nunca se creó una orden en la terminal. Point solo recibe órdenes por API cuando la terminal está en modo PDV (integrada / punto de venta). En modo Standalone, la terminal ignora tus POST /v1/orders por completo, así que jamás pasa a at_terminal, jamás se cobra, y por lo tanto no hay webhook que enviar.
Esta es la causa número uno de “mi orden nunca llega a la terminal”. Antes de culpar a tu endpoint, lista tus terminales con GET /terminals/v1/list y revisa el campo operating_mode:
curl -X GET \
"https://api.mercadopago.com/terminals/v1/list?limit=50&offset=0" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# revisa "operating_mode" de cada terminal: debe ser PDV, no Standalone
Si está en Standalone, cámbiala a PDV con el endpoint de actualización de modo operativo. Hasta que operating_mode sea PDV, ningún webhook va a llegar porque ninguna orden va a ejecutarse. El flujo completo de terminales está en cobrar con Mercado Pago Point (Orders API).
Las 3 causas de un vistazo
Idempotencia de tu lado: deduplica por order id porque los reintentos duplican
Ya sabes que la entrega es al menos una vez y que los reintentos cada 15 minutos reenvían el mismo evento. La consecuencia es dura: si tu handler imprime un ticket, libera el producto o dispara una factura CFDI cada vez que llega un evento, un solo cobro te genera duplicados.
La regla es deduplicar por el id de la orden (el data.id, con prefijo ORD). Persiste los ids ya procesados en una tabla con data.id como clave única —no un Set en memoria, que se pierde al reiniciar— y haz que reprocesar sea inofensivo.
async function encolarEvento(evento) {
const orderId = evento.data.id; // "ORD..."
const action = evento.action; // "order.processed", etc.
try {
// Inserta con (order_id, action) como clave única.
// Si ya existe, es un reintento de MP: no hagas nada dos veces.
await db.insert("webhook_events", { order_id: orderId, action });
} catch (e) {
if (esDuplicado(e)) return; // ya lo procesamos; el ACK 200 basta
throw e;
}
await encolarTrabajo({ orderId, action }); // worker: ticket, CFDI, liberar
}
Deduplicar por data.id cierra el círculo del fast-ack: aunque MP entregue el mismo order.processed cinco veces, actúas una sola. Si además concilias contra varios medios de pago, la lógica exactly-once end-to-end la extiendo en conciliación de pagos multi-rail en México. Y para emitir el comprobante al recibir order.processed, mira facturación CFDI automática desde Stripe/Mercado Pago.
Cómo probarlo: el simulador de notificaciones antes de tocar dinero real
No depures webhooks cobrando de verdad. Mercado Pago tiene un simulador de notificaciones en la configuración del webhook de tu aplicación: eliges el tópico (order) y dispara un POST a tu URL como en producción. Úsalo para validar tres cosas, en orden:
- Que tu endpoint es alcanzable por HTTPS y responde
200/201. Si el simulador reporta timeout, tu problema es de red o de tardanza, no de firma. - Que tu validación de
x-signaturepasa con el secret real de la aplicación. Aquí cazas el bug de prod antes de que sea de prod. - Que tu idempotencia aguanta. Dispara el mismo evento dos o tres veces y confirma que solo actúas una vez.
Cuando el simulador esté verde, pasa a una terminal en PDV con credenciales de test, y hasta el final a producción con dinero real. Para builds dentro de la terminal Android, el repo oficial es point-android_integration.
Checklist de depuración: de “no llega nada” a “order.processed confirmado”
Cuando te dicen “no me llegan los webhooks”, corre esto en orden. La primera que falle es tu causa:
- ¿La terminal está en PDV?
GET /terminals/v1/listy revisaoperating_mode. Si es Standalone, ninguna orden se ejecuta y no hay webhook. Cámbiala a PDV. - ¿La orden llegó a la terminal? Consulta la orden por su id
ORD; si elstatusnunca pasa decreatedaat_terminal, el problema es de terminal/modo, no de webhook. - ¿Tu endpoint responde
200/201en 22s? Si ves reintentos cada 15 minutos en los logs de MP, tu ACK está fallando. Aplica fast-ack: valida, encola, responde200, procesa async. - ¿La firma valida? Reconstruye el manifest con
data.id(minúsculas) +x-request-id+ts, HMAC-SHA256 con el secret de la aplicación, compara conv1. Prueba con el simulador. - ¿Deduplicas por
data.id? Si actúas en cada entrega, los reintentos te duplican tickets y facturas. Inserta condata.idcomo clave única. - ¿Confirmas con el simulador? Dispara
order.processeddesde la configuración del webhook antes de cobrar de verdad.
De 'no llega nada' a order.processed confirmado
Preguntas frecuentes sobre webhooks de Mercado Pago Point que no llegan
No me llega ninguna notificación. ¿Por dónde empiezo?
Por el modo de la terminal. Corre GET /terminals/v1/list y confirma que operating_mode sea PDV. En Standalone la terminal ignora tus órdenes, no se cobra nada y no hay webhook que enviar.
El webhook funcionaba en test y en producción no. ¿Qué pasó?
Casi siempre es el x-signature. Con el secret real, cualquier detalle del manifest importa: el data.id va en minúsculas, incluye el x-request-id y el ts, y usa el secret de esa aplicación específica. Revisa que no cruzaste secrets entre aplicaciones.
Veo la misma notificación varias veces. ¿Es un bug de MP?
No. La entrega es al menos una vez y, si tu endpoint no respondió 200/201 a tiempo, MP reintenta cada 15 minutos. Deduplica por data.id.
¿Puedo hacer polling al GET de la orden en vez de webhooks?
Como respaldo puntual sí; como mecanismo principal no. El resultado en tiempo real llega por el webhook con "type": "order". El polling te trae rate limiting, latencia y carreras.
¿Cuánto tiempo tengo para responder el webhook?
22 segundos, con un HTTP 200 o 201. Si tu handler tarda más porque genera CFDI o manda correos, mueve eso a un worker y responde el 200 de inmediato.
¿Qué evento me confirma que cobré?
order.processed, con "type": "order". En el payload, data.status será processed y data.total_paid_amount traerá el monto como string con dos decimales.
Si aún no armaste el cobro, empieza por cobrar con Mercado Pago Point (Orders API). Y si estás decidiendo qué pasarela usar en México, compara opciones en pasarelas de pago en México.