Webhooks de Mercado Pago en Hotelería: Manejo de Notificaciones IPN, Idempotencia y Sincronización en VikBooking
Aprende a implementar una arquitectura resiliente para procesar webhooks de Mercado Pago en sistemas hoteleros. Guía avanzada de verificación de firmas HMAC-SHA256, idempotencia y sincronización con VikBooking.
El procesamiento de pagos en la industria hotelera y turística presenta desafíos operativos que no existen en el e-commerce de productos físicos tradicionales. Cuando un huésped selecciona una suite para un fin de semana festivo, el inventario es finito y perecedero: una habitación bloqueada que no se cobra a tiempo representa una pérdida directa de facturación, mientras que una habitación confirmada dos veces por una condición de carrera (race condition) resulta en una sobreventa crítica que destruye la reputación del hotel.
Muchos motores de reserva basados en WordPress, como VikBooking, dependen de pasarelas de pago de terceros. En América Latina, Mercado Pago es el procesador dominante gracias a sus métodos de pago locales (tarjetas de crédito, débito, PSE en Colombia, Pix en Brasil, OXXO en México). Sin embargo, la integración no termina cuando el cliente ingresa su tarjeta en el checkout. La verdadera robustez de un sistema hotelero reside en cómo procesa de manera asíncrona las notificaciones de pago (Webhooks e IPN).
En este artículo técnico, analizaremos la arquitectura para recibir, validar criptográficamente y procesar webhooks de Mercado Pago garantizando estricta idempotencia, evitando estados inconsistentes de habitaciones y asegurando que las reservas en VikBooking se sincronicen en milisegundos.
1. El Problema Crítico del Inventario Hotelero y Pagos Asíncronos
En un checkout común de retail, si un webhook se retrasa 30 segundos, el usuario espera en la página de agradecimiento y el impacto es mínimo. En hotelería, el ciclo de vida de la reserva sigue una máquina de estados estricta:
[Cliente Inicia Reserva]
│
▼
[Estado: PENDING / Bloqueo Temporal (15 min)]
│
├───► (Webhook: payment.created / in_process) ──► Mantener bloqueo
│
├───► (Webhook: payment.approved) ──────────────► [Estado: CONFIRMED] + Emitir Voucher
│
├───► (Webhook: payment.rejected / cancelled) ──► [Estado: CANCELLED] + Liberar Habitación
│
└───► (Timeout 15 min sin webhook) ─────────────► Liberar Inventario en VikBooking
Si el servidor del hotel no maneja correctamente las notificaciones asíncronas de Mercado Pago, surgen tres problemas graves:
- Falsos Positivos de Timeout: El cliente paga exitosamente en la ventana de su banco vía PSE o Pix, pero el servidor del hotel tarda en recibir la notificación. El cronjob de VikBooking cancela la reserva por expiración y libera la habitación. Cuando el webhook finalmente llega, confirma una habitación que ya fue vendida a otro huésped.
- Duplicidad por Reintentos HTTP: Mercado Pago tiene una política agresiva de reintentos con exponential backoff. Si tu endpoint tarda más de 5000 ms en responder con un código HTTP
200o201, Mercado Pago reenviará el mismo evento repetidamente. Si tu código no es idempotente, podrías enviar 5 correos de confirmación al huésped y ejecutar múltiples llamadas redundantes a tu PMS. - Ataques de Inyección de Pagos (Spoofing): Si el endpoint de webhook no verifica la firma criptográfica HMAC que Mercado Pago envía en las cabeceras HTTP, un atacante podría enviar una petición
POSTsimulando una aprobación de pago con unpayment_idfalso, liberando una reserva sin haber pagado un solo centavo.
2. IPN Tradicional vs. Webhooks v2 de Mercado Pago
Mercado Pago ofrece históricamente dos mecanismos de notificación. Es indispensable comprender las diferencias técnicas para no implementar patrones obsoletos:
| Característica | IPN Clásico (Instant Payment Notification) | Webhooks v2 (Eventos en Tiempo Real) |
|---|---|---|
| Mecanismo de Envío | Query params en la URL (?id=123&topic=payment) | Payload JSON estructurado en el cuerpo del POST |
| Seguridad de Firma | No incluía firma nativa; requería consulta inversa manual | Cabecera x-signature con timestamp y hash HMAC-SHA256 |
| Granularidad de Eventos | Solo pagos y planes de suscripción | Múltiples tópicos: payment, chargebacks, merchant_order, etc. |
| Reintentos | Reintentos lineales simples | Reintentos exponenciales con control de latencia |
| Recomendación DoneAPI | Compatible por retrocompatibilidad, pero desaconsejado para nuevos desarrollos | Estándar mandatorio para producción y hotelería |
💡 Regla de Oro en Arquitectura Financiera: Nunca asumas el estado del pago únicamente leyendo el cuerpo del webhook. El webhook debe interpretarse únicamente como una señal de aviso: “Ocurrió un cambio en el recurso X”. Tu servidor debe validar la firma y luego realizar una petición autenticada
GET /v1/payments/{id}directamente a la API de Mercado Pago para obtener el estado oficial verificado.
3. Criptografía y Seguridad: Verificación de Firma HMAC-SHA256
Mercado Pago envía una cabecera HTTP llamada x-signature junto con cada notificación Webhook v2. Esta cabecera contiene dos componentes clave separados por comas:
ts: Un timestamp UNIX que indica el segundo exacto en que Mercado Pago emitió la notificación.v1: El hash HMAC-SHA256 generado con la clave secreta de webhook configurada en el panel de desarrolladores.
Algoritmo de Verificación Paso a Paso
- Extraer el valor de
tsyv1de la cabecerax-signature. - Validar que el timestamp
tsno tenga una diferencia mayor a 300 segundos (5 minutos) respecto a la hora actual del servidor. Esto previene ataques de repetición (Replay Attacks). - Construir la plantilla de datos (manifest template):
id:[data.id_del_evento];request-id:[x-request-id];ts:[timestamp]; - Generar el hash HMAC-SHA256 de esa cadena utilizando tu
WEBHOOK_SECRET_KEY. - Comparar el hash obtenido con el valor
v1usando una función de comparación de tiempo constante (hash_equalsen PHP ocrypto.timingSafeEqualen Node.js) para evitar ataques de canal lateral por análisis de tiempo (timing attacks).
4. Implementación en WordPress: Listener Seguro para VikBooking
A continuación presentamos la implementación en PHP para un plugin de WordPress o módulo personalizado que escucha los webhooks de Mercado Pago e interactúa directamente con el motor de VikBooking.
<?php
/**
* Plugin Name: DoneAPI - Mercado Pago Webhook Handler para VikBooking
* Description: Listener de webhooks con validación HMAC-SHA256 e idempotencia para reservas de VikBooking.
* Version: 2.1.0
* Author: DoneAPI Engineering Team
*/
if (!defined('ABSPATH')) {
exit;
}
add_action('rest_api_init', function () {
register_rest_route('doneapi/v1', '/mercadopago/webhook', [
'methods' => 'POST',
'callback' => 'doneapi_handle_mercadopago_webhook',
'permission_callback' => '__return_true', // La autenticación se valida criptográficamente en el callback
]);
});
/**
* Manejador principal del webhook
*/
function doneapi_handle_mercadopago_webhook(WP_REST_Request $request) {
$signature_header = $request->get_header('x-signature');
$request_id = $request->get_header('x-request-id');
$body = $request->get_json_params();
if (empty($signature_header) || empty($request_id) || empty($body)) {
return new WP_REST_Response(['error' => 'Missing security headers or payload'], 400);
}
$webhook_secret = defined('MERCADOPAGO_WEBHOOK_SECRET') ? MERCADOPAGO_WEBHOOK_SECRET : get_option('doneapi_mp_webhook_secret');
// 1. Extraer ts y v1
$parts = explode(',', $signature_header);
$ts = null;
$v1 = null;
foreach ($parts as $part) {
$subparts = explode('=', trim($part), 2);
if (count($subparts) === 2) {
if ($subparts[0] === 'ts') $ts = $subparts[1];
if ($subparts[0] === 'v1') $v1 = $subparts[1];
}
}
if (!$ts || !$v1) {
return new WP_REST_Response(['error' => 'Malformed x-signature header'], 400);
}
// 2. Prevenir Replay Attacks (tolerancia: 5 minutos)
if (abs(time() - intval($ts)) > 300) {
return new WP_REST_Response(['error' => 'Timestamp tolerance exceeded (Replay Attack Guard)'], 401);
}
// 3. Extraer el ID de la entidad
$entity_id = isset($body['data']['id']) ? $body['data']['id'] : null;
if (!$entity_id) {
return new WP_REST_Response(['error' => 'Invalid data payload'], 400);
}
// 4. Validar firma criptográfica
$manifest = "id:{$entity_id};request-id:{$request_id};ts:{$ts};";
$calculated_hash = hash_hmac('sha256', $manifest, $webhook_secret);
if (!hash_equals($calculated_hash, $v1)) {
error_log("[DoneAPI Security Alert] Firma inválida recibida en webhook de Mercado Pago para ID: {$entity_id}");
return new WP_REST_Response(['error' => 'Invalid HMAC signature'], 401);
}
// 5. Procesamiento asíncrono o desacoplado
// Responder inmediatamente con 200 OK a Mercado Pago para evitar reintentos innecesarios
// y procesar el evento asegurando idempotencia
$topic = isset($body['type']) ? $body['type'] : (isset($body['action']) ? $body['action'] : 'payment');
if ($topic === 'payment' || strpos($topic, 'payment.') === 0) {
doneapi_process_hotel_payment($entity_id);
}
return new WP_REST_Response(['status' => 'acknowledged'], 200);
}
/**
* Consulta la API oficial de Mercado Pago y actualiza la reserva en VikBooking
*/
function doneapi_process_hotel_payment($payment_id) {
global $wpdb;
// Control de Idempotencia a nivel de base de datos
// Evita procesar dos veces el mismo payment_id si Mercado Pago reintenta la llamada
$table_logs = $wpdb->prefix . 'doneapi_mp_processed_events';
// Verificamos si ya existe una transacción finalizada para este payment_id
$already_processed = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table_logs} WHERE payment_id = %s AND status = 'COMPLETED'",
$payment_id
));
if ($already_processed) {
return; // Idempotencia garantizada: ya fue procesado con éxito
}
$access_token = defined('MERCADOPAGO_ACCESS_TOKEN') ? MERCADOPAGO_ACCESS_TOKEN : get_option('doneapi_mp_access_token');
// Consulta segura a Mercado Pago
$response = wp_remote_get("https://api.mercadopago.com/v1/payments/{$payment_id}", [
'headers' => [
'Authorization' => "Bearer {$access_token}",
'Content-Type' => 'application/json',
],
'timeout' => 15,
]);
if (is_wp_error($response)) {
error_log("[DoneAPI Error] No fue posible conectar con Mercado Pago API: " . $response->get_error_message());
return;
}
$payment_data = json_decode(wp_remote_retrieve_body($response), true);
if (!$payment_data || !isset($payment_data['status'])) {
return;
}
$status = $payment_data['status']; // approved, pending, rejected, refunded, cancelled
$external_ref = isset($payment_data['external_reference']) ? $payment_data['external_reference'] : null;
if (!$external_ref) {
error_log("[DoneAPI Warning] Pago {$payment_id} no contiene external_reference para asociar a VikBooking.");
return;
}
// En nuestra implementación, external_reference contiene el ID de la reserva en VikBooking (ej. "VB-4589")
$booking_id = intval(str_replace('VB-', '', $external_ref));
if ($booking_id <= 0) {
return;
}
// Integración con VikBooking Core
if (class_exists('VikBooking')) {
$vbo = VikBooking::getBooking($booking_id);
if ($vbo) {
if ($status === 'approved') {
// Confirmar reserva en VikBooking, marcar pagada y disparar correos de voucher
VikBooking::setBookingStatus($booking_id, 'confirmed');
VikBooking::addPaymentLog($booking_id, [
'gateway' => 'Mercado Pago',
'transaction_id'=> $payment_id,
'amount' => $payment_data['transaction_amount'],
'status' => 'SUCCESS'
]);
// Registrar evento completado para idempotencia
$wpdb->replace($table_logs, [
'payment_id' => $payment_id,
'booking_id' => $booking_id,
'status' => 'COMPLETED',
'processed_at' => current_time('mysql')
], ['%s', '%d', '%s', '%s']);
} elseif (in_array($status, ['rejected', 'cancelled'])) {
// Liberar la habitación y cancelar la orden en VikBooking
VikBooking::setBookingStatus($booking_id, 'cancelled');
}
}
}
}
5. Patrón de Idempotencia y Manejo de Concurrencia
El término Idempotencia significa que ejecutar una operación múltiples veces produce el mismo resultado que ejecutarla una sola vez. En un entorno hotelero distribuido, la idempotencia debe garantizarse en tres capas distintas:
[Mercado Pago Webhook Delivery]
│
▼ (Capa 1: Red y HTTP)
Valida ts y rechaza repeticiones (> 300s)
│
▼ (Capa 2: Bloqueo Distribuido / Redis o Mutex)
Lock con clave: "lock:payment:{payment_id}"
│
▼ (Capa 3: Persistencia Relacional / ACID)
INSERT INTO logs (payment_id, booking_id) VALUES (...)
ON DUPLICATE KEY UPDATE status = ...
1. El Riesgo de los Reintentos Concurrentes
Supongamos que la red sufre una fluctuación momentánea. Mercado Pago envía la notificación a las 14:00:00 y, al no recibir confirmación en 3 segundos, dispara un reintento a las 14:00:03. Ambos procesos PHP se ejecutan en paralelo en el servidor web.
Sin un bloqueo atómico o una restricción de unicidad en la base de datos (UNIQUE KEY (payment_id)), ambos hilos podrían intentar actualizar la reserva de VikBooking al mismo tiempo, generando dos entradas de cobro en los balances contables del PMS.
2. Tablas Dedicadas de Control de Transacciones
Para proteger WordPress de sobrecarga, recomendamos crear una tabla dedicada con motor InnoDB:
CREATE TABLE `wp_doneapi_mp_processed_events` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`payment_id` varchar(64) NOT NULL,
`booking_id` int(11) unsigned NOT NULL,
`status` enum('PENDING','PROCESSING','COMPLETED','FAILED') NOT NULL DEFAULT 'PENDING',
`payload_hash` char(64) DEFAULT NULL,
`processed_at` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_payment_id` (`payment_id`),
KEY `idx_booking_id` (`booking_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
6. Monitoreo, Pruebas y Depuración en Producción
El error más común al integrar pasarelas de pago en hoteles es probar únicamente en entornos locales y asumir que en producción todo se comportará igual. Para garantizar una puesta en marcha impecable:
- Simulación de Reintentos de Red: Utiliza herramientas como
ngroko la CLI oficial de Mercado Pago para inspeccionar los payloads reales y simular caídas del listener forzando respuestas HTTP500para verificar que el mecanismo de recuperación funcione. - Registro Centralizado de Trazas: No utilices
error_logconvencional con información sensible del tarjetahabiente (nunca guardes números de tarjeta o CVV). Registra únicamente elpayment_id, el código de respuesta HTTP de la API de Mercado Pago y el tiempo de ejecución en milisegundos. - Manejo de Devoluciones y Contracargos (Chargebacks): Cuando un cliente disputa un cargo ante su banco, Mercado Pago emite un webhook de contracargo. Tu listener debe interceptar este evento, marcar la reserva en alerta en VikBooking y notificar de inmediato al departamento de recepción o administración del hotel.
7. Solución Lista para Producción: Plugin DoneAPI VikBooking Mercado Pago ($7 USD)
Si gestionas un hotel, hostal o agencia de alquiler vacacional y necesitas conectar VikBooking con Mercado Pago sin incurrir en meses de desarrollo a medida ni lidiar con librerías complejas, en DoneAPI hemos creado y empaquetado la solución definitiva:
- Integración Nativa con VikBooking: Compatible con las últimas versiones de VikBooking para WordPress.
- Verificación Criptográfica Automática: Manejo transparente de cabeceras
x-signaturey Webhooks v2. - Soporte para Monedas Locales de LATAM: Pagos en pesos colombianos (COP), pesos mexicanos (MXN), reales brasileños (BRL), pesos argentinos (ARS) y más.
- Cero Comisiones Recurrentes por Reserva: Licencia directa de código por un pago único de $7 USD.
💬 ¿Quieres implementar el plugin oficial en tu sitio o requieres una integración personalizada para tu cadena hotelera?
Escríbenos directamente por WhatsApp y nuestro equipo de ingenieros te asistirá de inmediato en la configuración y puesta en marcha.
Instala el Plugin VikBooking Mercado Pago ($7 USD) o Solicita Asesoría
Optimiza la recaudación de tu hotel con confirmaciones instantáneas por webhook y elimina sobreventas y cancelaciones fallidas.
8. Conclusión
El diseño de una pasarela de pagos para el sector hotelero no tolera aproximaciones simplistas. La combinación de la firma criptográfica HMAC-SHA256, la validación activa contra el endpoint /v1/payments/{id} de Mercado Pago y el uso de registros con clave única para asegurar idempotencia matemática permite procesar cientos de reservas simultáneas con absoluta estabilidad.
Al implementar estas prácticas en VikBooking, eliminas por completo el riesgo de overbooking por reintentos de red y garantizas a tus huéspedes una experiencia de reserva confiable, segura y veloz.