Mercado Pago Webhooks in Hospitality: IPN Processing, Idempotency & VikBooking Synchronization
Architect a resilient system for processing Mercado Pago webhooks in hotel booking engines. HMAC-SHA256 verification, idempotency locks, and VikBooking synchronization.
Payment processing within the hospitality and travel industry presents operational hazards virtually absent in traditional physical e-commerce. When a guest reserves a boutique hotel suite for a holiday weekend, that inventory is finite and perishable: a room held in limbo that fails to clear payment represents lost revenue, while a room confirmed twice due to a concurrency race condition causes disastrous overbooking that destroys guest trust.
Many WordPress-based booking engines, such as VikBooking, rely on third-party payment gateways. In Latin America, Mercado Pago is the undisputed market leader thanks to its seamless integration with regional payment rails (credit cards, debit cards, PSE in Colombia, Pix in Brazil, SPEI & OXXO in Mexico). However, the checkout transaction does not conclude when a traveler submits their card details. The true resilience of a hospitality platform depends entirely on how it processes asynchronous payment notifications (Webhooks and IPN).
In this engineering guide, we dissect the architecture required to ingest, cryptographically verify, and process Mercado Pago webhooks while enforcing strict idempotency, preventing inventory race conditions, and synchronizing VikBooking reservations in sub-second intervals.
1. The Perishable Inventory Problem in Hospitality Checkout Flows
In standard retail e-commerce, a 30-second webhook delay while a customer waits on a confirmation page is largely benign. In hospitality, reservation lifecycles follow a strict state machine:
[Guest Initiates Checkout]
│
▼
[State: PENDING / Temporary Room Hold (15 min)]
│
├───► (Webhook: payment.created / in_process) ──► Retain temporary hold
│
├───► (Webhook: payment.approved) ──────────────► [State: CONFIRMED] + Issue Voucher
│
├───► (Webhook: payment.rejected / cancelled) ──► [State: CANCELLED] + Release Room
│
└───► (15-min timeout with zero webhook) ───────► Release Room Dates in VikBooking
If the hotel backend fails to handle asynchronous Mercado Pago notifications defensively, three critical failures emerge:
- Timeout False Positives: A guest pays via bank transfer (PSE or Pix), but server latency delays webhook delivery. VikBooking’s background cron cancels the expired reservation and re-opens room availability. When the webhook finally arrives, it confirms a room that has already been resold to another guest.
- Duplication via HTTP Retry Storms: Mercado Pago enforces an aggressive exponential backoff retry policy. If your webhook listener takes longer than 5,000 milliseconds to respond with HTTP
200or201, Mercado Pago resends the identical event. Non-idempotent code risks dispatching multiple confirmation emails and corrupting PMS ledgers. - Payment Spoofing Injections: If your endpoint fails to cryptographically verify the HMAC signature dispatched in the request headers, an attacker can submit fabricated
POSTpayloads mimicking approved payments, unlocking reservations without paying a single dollar.
2. Legacy IPN vs. Mercado Pago Webhooks v2
Mercado Pago provides two distinct notification mechanisms. Understanding the technical divergence is crucial for avoiding obsolete legacy patterns:
| Technical Feature | Legacy IPN (Instant Payment Notification) | Modern Webhooks v2 (Real-Time Events) |
|---|---|---|
| Delivery Mechanism | Query parameters in the URL (?id=123&topic=payment) | Structured JSON payload in the HTTP POST body |
| Signature Cryptography | No native header signature; demanded manual reverse lookups | Mandatory x-signature header with timestamp & HMAC-SHA256 |
| Event Granularity | Limited to basic payments and subscription plans | Rich event taxonomy: payment, chargebacks, merchant_order |
| Retry Mechanics | Rigid linear retries | Resilient exponential backoff with latency tracking |
| DoneAPI Recommendation | Maintained for legacy compatibility; strictly discouraged | Mandatory production standard for enterprise hospitality |
💡 Golden Rule in Fintech Architecture: Never deduce the final financial state solely from the webhook body. The webhook should be interpreted strictly as an advisory signal: “State changed for resource X”. Your server must verify the cryptographic signature and then execute an authenticated
GET /v1/payments/{id}call directly to Mercado Pago’s REST API to retrieve the verified ledger state.
3. Cryptographic Security: Verifying HMAC-SHA256 Signatures
Mercado Pago transmits an HTTP header named x-signature alongside each Webhook v2 dispatch. This header contains two key components separated by commas:
ts: A UNIX timestamp indicating the exact second Mercado Pago dispatched the payload.v1: The HMAC-SHA256 hash computed with your private webhook secret key.
Verification Algorithm:
- Parse
tsandv1from thex-signatureheader. - Verify that
tsis within 300 seconds (5 minutes) of the current server timestamp. This completely eliminates Replay Attacks. - Reconstruct the raw manifest template:
id:[event_data_id];request-id:[x-request-id];ts:[timestamp]; - Compute the HMAC-SHA256 hash and execute a timing-attack safe comparison using
hash_equals().
4. Production PHP Listener Implementation for VikBooking
The following implementation illustrates how to handle inbound webhooks with cryptographic validation, API state verification, and safe VikBooking reservation mutation:
<?php
declare(strict_types=1);
namespace DoneApi\Plugin\Gateways;
use WP_REST_Request;
use WP_REST_Response;
class MercadoPagoWebhookHandler {
private string $secret_key;
private string $access_token;
public function __construct(string $secret_key, string $access_token) {
$this->secret_key = $secret_key;
$this->access_token = $access_token;
}
public function process_inbound_webhook(WP_REST_Request $request): WP_REST_Response {
$signature_header = $request->get_header('x-signature') ?? '';
$request_id = $request->get_header('x-request-id') ?? '';
$body = $request->get_json_params() ?? [];
// 1. Verify HMAC Cryptographic Signature
if (!$this->verify_signature($signature_header, $request_id, $body)) {
return new WP_REST_Response(['error' => 'Invalid HMAC Signature'], 401);
}
$event_type = $body['type'] ?? '';
$payment_id = $body['data']['id'] ?? null;
if ($event_type !== 'payment' || !$payment_id) {
return new WP_REST_Response(['status' => 'ignored'], 200);
}
// 2. Query Verified Transaction State from Mercado Pago REST API
$payment_data = $this->fetch_verified_payment((string) $payment_id);
if (!$payment_data) {
return new WP_REST_Response(['error' => 'Payment not found in gateway'], 404);
}
$booking_id = $payment_data['external_reference'] ?? null;
$status = $payment_data['status'] ?? '';
if (!$booking_id) {
return new WP_REST_Response(['status' => 'missing_external_reference'], 200);
}
// 3. Acquire Distributed Lock & Apply Idempotent Mutation
$this->synchronize_vikbooking((int) $booking_id, (string) $payment_id, $status, $payment_data);
return new WP_REST_Response(['status' => 'acknowledged'], 200);
}
private function verify_signature(string $signature_header, string $request_id, array $body): bool {
if (empty($signature_header)) return false;
$parts = [];
foreach (explode(',', $signature_header) as $part) {
[$key, $val] = explode('=', trim($part), 2);
$parts[$key] = $val;
}
$ts = $parts['ts'] ?? null;
$v1 = $parts['v1'] ?? null;
if (!$ts || !$v1 || (abs(time() - (int) $ts) > 300)) {
return false; // Replay attack guard
}
$data_id = $body['data']['id'] ?? '';
$manifest = "id:{$data_id};request-id:{$request_id};ts:{$ts};";
$expected = hash_hmac('sha256', $manifest, $this->secret_key);
return hash_equals($expected, $v1);
}
private function fetch_verified_payment(string $payment_id): ?array {
$response = wp_remote_get("https://api.mercadopago.com/v1/payments/{$payment_id}", [
'headers' => [
'Authorization' => "Bearer {$this->access_token}",
'Accept' => 'application/json',
],
'timeout' => 10,
]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
return null;
}
return json_decode(wp_remote_retrieve_body($response), true);
}
private function synchronize_vikbooking(int $booking_id, string $payment_id, string $status, array $data): void {
global $wpdb;
$table_events = $wpdb->prefix . 'doneapi_mp_processed_events';
// Enforce strict relational idempotency
$inserted = $wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$table_events} (payment_id, booking_id, status, processed_at) VALUES (%s, %d, %s, NOW())",
$payment_id,
$booking_id,
$status
));
// If duplicate execution detected, skip PMS status mutations
if ($inserted === 0) {
return;
}
if ($status === 'approved') {
// Confirm room booking in VikBooking and dispatch guest confirmation voucher
if (class_exists('\VikBooking')) {
\VikBooking::setBookingStatus($booking_id, 'confirmed');
}
} elseif (in_array($status, ['rejected', 'cancelled'])) {
// Unlock room inventory in VikBooking
if (class_exists('\VikBooking')) {
\VikBooking::setBookingStatus($booking_id, 'cancelled');
}
}
}
}
5. Three-Tier Idempotency Pattern
Idempotency guarantees that executing an operation multiple times yields the exact same state as executing it once. In a distributed hospitality environment, idempotency is enforced across three sequential tiers:
[Mercado Pago Webhook Ingress]
│
▼ (Tier 1: Network & Clock Skew Guard)
Validate ts timestamp & reject replay attempts (> 300s)
│
▼ (Tier 2: Distributed Lock / In-Memory Mutex)
Acquire Redis Mutex: "lock:payment:{payment_id}"
│
▼ (Tier 3: Relational ACID Persistence)
INSERT IGNORE INTO wp_doneapi_mp_processed_events (payment_id, booking_id)
Skip execution if record already exists
Dedicated Event Auditing Table
To protect WordPress from state drift, we maintain a dedicated transaction table inside MySQL (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',
`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. Turnkey Production Solution: DoneAPI VikBooking Mercado Pago Plugin ($7 USD)
If you manage a hotel, resort, boutique glamping retreat, or property rental agency and need to connect VikBooking with Mercado Pago without months of bespoke development, DoneAPI provides the definitive production-ready plugin:
- Native VikBooking Integration: Full compatibility with the latest versions of VikBooking for WordPress.
- Automated Cryptographic Verification: Native validation of
x-signatureheaders and Webhooks v2. - Comprehensive LATAM Currency Rails: Multi-currency support for Colombian Pesos (COP), Mexican Pesos (MXN), Brazilian Reais (BRL), and US Dollars (USD).
- Zero Recurring Subscription Fees: Permanent software license for a one-time payment of $7 USD.
💬 Looking to install the official plugin on your site or require custom enterprise architecture for your hotel chain? Reach out directly via WhatsApp for setup support and turnkey onboarding.
Get the VikBooking Mercado Pago Plugin ($7 USD) or Request Advisory
Accelerate your hotel’s direct booking revenue with instant webhook reconciliation, zero overbooking, and seamless checkout.
7. Conclusion
Designing an enterprise payment integration for the hospitality sector tolerates zero shortcuts. Combining HMAC-SHA256 signature verification, active reverse lookups against Mercado Pago’s /v1/payments/{id} endpoint, and strict database idempotency constraints allows your booking engine to process hundreds of concurrent reservations with mathematical certainty.
By embedding these architectural standards into VikBooking, you completely eliminate overbooking risks from network retries and provide travelers with a fast, reliable, and secure direct checkout experience.