Una notificación de pago puede llegar más de una vez. También puede aparecer cuando mi aplicación ya recibió una actualización más reciente. Si cada entrega dispara otra acreditación, un reintento puede convertirse en un problema de dinero y conciliación.
Preparé una muestra propia en ASP.NET Core 8 para explorar ese flujo: autenticar la notificación, consultar el pago y actualizar una orden sin repetir trabajo ante una entrega idéntica. Sus ocho pruebas pasan en Windows sobre .NET 8, con la compilación Release sin advertencias ni errores. La muestra utiliza memoria y una API simulada; todavía no demuestra una integración real con Mercado Pago.
Estos son el recorrido, las comprobaciones y los límites que mantengo visibles antes de adaptar el ejemplo a un negocio.
Separar la notificación del estado del pago
Mi endpoint recibe un identificador, pero no toma el estado del pago ni la orden asociada del cuerpo de la notificación. Esos datos deben venir de una consulta autenticada a la API de pagos.
La interfaz IPaymentsApi devuelve el identificador del pago, la orden local verificada, el estado y la fecha de actualización. El adaptador debe resolver la referencia del proveedor contra mis registros. Una firma correcta no reemplaza esa comprobación de negocio.
La muestra no incluye ese adaptador real. Registra una implementación que devuelve un error y responde 503 ante una notificación autenticada. Para usarla con pagos reales, primero tendría que implementar y validar la consulta con credenciales guardadas fuera del código.
Validar la entrada antes de procesarla
POST /mercadopago/webhook acepta data.id desde la consulta o desde un objeto JSON. Si ambas ubicaciones incluyen el identificador, exige que coincidan exactamente. Una discrepancia produce 400 en lugar de elegir silenciosamente uno de los valores.
También exige un solo encabezado x-request-id y uno x-signature. El procesador interpreta una firma con componentes ts y v1, rechaza componentes desconocidos o repetidos y comprueba que el resumen hexadecimal tenga la longitud esperada.
Son reglas concretas de esta implementación. No presento el ejemplo como compatible con cualquier variante de notificación: esa compatibilidad sigue necesitando pruebas con entregas reales.
Normalizar el identificador dentro del manifiesto
La firma se calcula sobre un manifiesto, no sobre todo el JSON recibido. En esta muestra, el valor de data.id se convierte a minúsculas al construir ese texto. El identificador de solicitud conserva sus mayúsculas y minúsculas.
Este es el primer fragmento de mi archivo Webhooks.cs:
manifest = $"id:{input.PaymentId.ToLowerInvariant()};request-id:{input.RequestId};ts:{timestamp};";
var expected = HMACSHA256.HashData(secret, Encoding.UTF8.GetBytes(manifest));
return CryptographicOperations.FixedTimeEquals(expected, supplied);
El punto y coma final forma parte del manifiesto. Uso UTF-8, HMAC-SHA256 y comparación de los bytes en tiempo constante. El secreto de webhooks es distinto del token utilizado para consultar pagos.
La normalización no modifica el identificador que envío al adaptador ni el que guardo en la orden. El octavo caso usa ABC123XYZ, calcula la firma independientemente con abc123xyz y conserva MixedCaseRequest8 como identificador de solicitud. Así comprueba que solo se normalice el valor destinado al manifiesto.
El procesador también exige que la marca de tiempo esté dentro de una tolerancia configurable, de cinco minutos por defecto hacia el pasado o el futuro. El reloj es inyectable para probar ese comportamiento sin depender de la hora de ejecución.
Reconocer una repetición exacta
Después de autenticar, el procesador adquiere un semáforo y revisa un conjunto de manifiestos procesados. Este segundo fragmento muestra la salida temprana:
if (processed.Contains(manifest))
return new(200, "duplicate");
Una repetición exacta dentro de la ventana válida recibe 200 sin volver a consultar la API. La autenticación ocurre antes: una entrega vencida recibe 401 aunque su manifiesto haya sido procesado previamente.
Si cambia el identificador de solicitud o la marca de tiempo, cambia la clave y se consulta nuevamente el pago. Eso permite observar actualizaciones posteriores del mismo pago, como el paso de pendiente a aprobado. Deduplicar permanentemente solo por identificador de pago impediría aplicar esa transición.
Cuando el adaptador falla, respondo 503 sin registrar el manifiesto como procesado. De esa manera, el intento fallido no bloquea una entrega posterior.
Evitar que una actualización vieja haga retroceder la orden
Una entrega nueva puede traer una consulta cuyo resultado sea anterior al estado guardado. Comparo la fecha de actualización obtenida de la API, no la hora de llegada del webhook ni la marca de su firma.
El tercer fragmento contiene las reglas de retroceso:
var stale = payment.UpdatedAt < order.UpdatedAt;
var wouldRegress =
(order.Status == "approved"
&& payment.Status is "pending" or "rejected")
|| (order.Status == "refunded" && payment.Status != "refunded");
Un resultado anterior o una transición prohibida recibe 200 con ignored_stale_or_regressive. La orden conserva su estado. Un resultado actual o posterior puede llevar una aprobación a reembolso; una orden reembolsada no vuelve a otro estado con estas reglas.
La muestra admite un pago por orden. Si aparece otro identificador para una orden ya asociada, responde 409. Es una decisión del ejemplo que habría que revisar para negocios con varios intentos de pago.
Qué cubren las ocho pruebas
Las ocho pruebas pasan en Windows sobre .NET 8, con la compilación Release sin advertencias ni errores.
Los casos cubren pago aprobado, firma incorrecta, firma vencida, notificación duplicada, transición de pendiente a aprobado, pago rechazado, aprobación seguida de un resultado pendiente antiguo y normalización del identificador alfanumérico.
El ejecutor llama directamente al procesador con un reloj fijo y una API falsa que cuenta consultas. El caso duplicado verifica una sola consulta después de dos entregas. Los casos de firma inválida o vencida comprueban que no se consulte la API ni se modifique la orden.
No se inicia un servidor HTTP ni se realizan llamadas externas. Tampoco hay pruebas específicas de reembolsos, concurrencia distribuida o recuperación después de un reinicio.
Qué falta para una acreditación durable
Las órdenes y las claves procesadas viven en memoria. Un reinicio borra ambas, y el conjunto de claves crece sin una política de retención. El semáforo coordina una instancia y mantiene serializadas incluso las consultas al adaptador; no coordina varios procesos.
Además, actualizar un estado no equivale a acreditar dinero una sola vez. El ejemplo no ejecuta una acreditación contable, un envío ni una transferencia. Para incorporar esos efectos necesitaría almacenamiento durable, restricciones únicas y una transacción que vincule la protección contra duplicados con el cambio de negocio. Las acciones externas requieren su propia estrategia de recuperación.
Los tres fragmentos pertenecen a mi muestra propia, sin repositorio público. La considero una base verificable para discutir el flujo, con integración real y persistencia todavía pendientes.
Si necesitas recibir webhooks de Mercado Pago en tu API de ASP.NET Core, escríbeme sobre tu flujo de pagos y los reintentos que necesitas resolver.
Top comments (0)