Skip to main content
Webhooks are HTTP POST notifications that MercadoPago sends to your server when payment events occur. They are essential for reliable payment processing.

Why webhooks matter

Never rely solely on redirect URLs or frontend callbacks for payment confirmation. Users can close browsers, lose connection, or manipulate client-side code. Webhooks provide server-to-server communication that you can trust.
Webhooks are critical because they:
  • Provide definitive payment status updates
  • Work even if users close their browser
  • Can’t be manipulated by end users
  • Handle asynchronous payment methods (bank transfers, etc.)
  • Notify you of refunds and chargebacks

Webhook endpoint

The package automatically registers a webhook endpoint:
POST /api/mercadopago/webhooks
This route is always active, even when demo routes are disabled in production.

Configuration

Setting up the webhook secret

For security, configure your webhook secret in .env:
MERCADOPAGO_WEBHOOK_SECRET=your_webhook_secret_from_mercadopago
Get your webhook secret from your MercadoPago dashboard:
  1. Go to Your integrationsWebhooks
  2. Create or edit a webhook configuration
  3. Copy the secret key provided
Without a webhook secret, the package accepts all webhook requests but marks them as validated: false. Always configure the secret in production.

Registering your webhook URL

In your MercadoPago dashboard, register your webhook URL:
https://your-domain.com/api/mercadopago/webhooks
You can also set the notification URL per preference or payment:
$preference = $preferenceService->create([
    'items' => [...],
    'notification_url' => 'https://your-domain.com/api/mercadopago/webhooks',
]);

Webhook signature validation

The package automatically validates webhook signatures using HMAC-SHA256:
1

Extract signature components

Parse x-signature header for timestamp (ts) and signature hash (v1)
2

Build manifest

Construct manifest string: id:{resource_id};request-id:{request_id};ts:{timestamp};
3

Compute HMAC

Calculate HMAC-SHA256 of manifest using webhook secret
4

Compare signatures

Use timing-safe comparison to verify signatures match

Validation implementation

The validation logic in src/Services/WebhookService.php:38-60:
private function assertValidSignature(
    Request $request,
    array $payload,
    string $secret,
    string $signatureHeader,
): void {
    $signatureParts = $this->parseSignatureHeader($signatureHeader);
    $resourceId = (string) ($request->query('data.id') ?? Arr::get($payload, 'data.id', ''));
    $requestId = (string) $request->header('x-request-id', '');

    $manifest = sprintf(
        'id:%s;request-id:%s;ts:%s;',
        $resourceId,
        $requestId,
        $signatureParts['ts'],
    );

    $computedHash = hash_hmac('sha256', $manifest, $secret);

    if (! hash_equals($computedHash, $signatureParts['v1'])) {
        throw InvalidWebhookSignatureException::signatureMismatch();
    }
}

Webhook payload structure

MercadoPago sends webhooks with this structure:
{
  "type": "payment",
  "data": {
    "id": "123456789"
  }
}

Common webhook types

TypeDescription
paymentPayment status changed
merchant_orderOrder status changed
subscriptionSubscription event
invoiceInvoice event

Query parameters

Webhooks may also send data via query string:
POST /webhooks?topic=payment&data.id=123456789

Processing webhooks

The WebhookService returns a normalized response:
[
    'acknowledged' => true,
    'validated' => true,  // true if signature was verified
    'topic' => 'payment',
    'resource' => '123456789',  // The payment/order/subscription ID
    'payload' => [...],  // Full webhook payload
]

Default controller implementation

The package’s WebhookController (line 17-28 in source):
public function store(
    StoreWebhookRequest $request,
    WebhookService $webhookService
): JsonResponse {
    $result = $webhookService->handle($request);

    return $this->respondWithSuccess($result);
}

Custom webhook handling

Option 1: Dispatch events

Create a custom controller that dispatches Laravel events:
<?php

namespace App\Http\Controllers;

use Fitodac\LaravelMercadoPago\Services\WebhookService;
use App\Events\PaymentReceived;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class MercadoPagoWebhookController
{
    public function __invoke(
        Request $request,
        WebhookService $webhookService
    ): JsonResponse {
        $result = $webhookService->handle($request);

        if ($result['topic'] === 'payment') {
            event(new PaymentReceived(
                paymentId: $result['resource'],
                validated: $result['validated'],
            ));
        }

        return response()->json(['ok' => true]);
    }
}

Option 2: Queue jobs

Dispatch jobs for asynchronous processing:
use App\Jobs\ProcessPaymentWebhook;

public function __invoke(
    Request $request,
    WebhookService $webhookService
): JsonResponse {
    $result = $webhookService->handle($request);

    if (!$result['validated']) {
        return response()->json(['error' => 'Invalid signature'], 401);
    }

    ProcessPaymentWebhook::dispatch($result['resource'])
        ->onQueue('webhooks');

    return response()->json(['ok' => true]);
}

Option 3: Direct processing

Process webhooks inline (ensure fast response):
use Fitodac\LaravelMercadoPago\Services\PaymentService;
use App\Models\Order;

public function __invoke(
    Request $request,
    WebhookService $webhookService,
    PaymentService $paymentService
): JsonResponse {
    $result = $webhookService->handle($request);

    if ($result['topic'] === 'payment') {
        // Fetch current payment status from MercadoPago
        $payment = $paymentService->get($result['resource']);
        
        // Update order status
        $externalRef = data_get($payment, 'external_reference');
        $order = Order::where('reference', $externalRef)->first();
        
        if ($order) {
            $order->update([
                'payment_status' => data_get($payment, 'status'),
                'payment_id' => data_get($payment, 'id'),
            ]);
        }
    }

    return response()->json(['ok' => true]);
}

Best practices

Always verify payment status

Don’t trust webhook payload data directly. Always fetch the current payment status from MercadoPago’s API to prevent spoofing.
// ❌ Don't do this
$status = $result['payload']['status'];

// ✅ Do this instead
$payment = $paymentService->get($result['resource']);
$status = data_get($payment, 'status');

Respond quickly

MercadoPago expects a quick response (< 5 seconds):
// Dispatch job for processing
ProcessPaymentWebhook::dispatch($paymentId);

// Return immediately
return response()->json(['ok' => true]);

Handle idempotency

MercadoPago may send duplicate webhooks. Use idempotency checks:
if (WebhookLog::where('resource_id', $paymentId)->exists()) {
    // Already processed
    return response()->json(['ok' => true]);
}

WebhookLog::create([
    'resource_id' => $paymentId,
    'topic' => $result['topic'],
    'processed_at' => now(),
]);

// Process webhook...

Log everything

Maintain comprehensive webhook logs:
\Log::info('Webhook received', [
    'topic' => $result['topic'],
    'resource' => $result['resource'],
    'validated' => $result['validated'],
    'payload' => $result['payload'],
]);

Testing webhooks locally

Using tunneling services

Expose your local server to the internet:
# Using ngrok
ngrok http 8000

# Using expose
expose share http://localhost:8000
Then register the public URL in MercadoPago:
https://abc123.ngrok.io/api/mercadopago/webhooks

Manual webhook simulation

Test your webhook endpoint manually:
curl --request POST \
  --url http://localhost:8000/api/mercadopago/webhooks \
  --header 'Content-Type: application/json' \
  --header 'x-request-id: test-request-123' \
  --data '{
    "type": "payment",
    "data": {
      "id": "123456789"
    }
  }'
Without a valid signature, the webhook will be acknowledged but marked as validated: false.

Error responses

Invalid signature (401)

When signature validation fails:
{
  "ok": false,
  "message": "Invalid webhook signature"
}

Missing configuration (422)

When MercadoPago credentials are not configured:
{
  "ok": false,
  "message": "Mercado Pago access token is not configured."
}

Production checklist

  • MERCADOPAGO_WEBHOOK_SECRET configured
  • ✅ HTTPS enabled on webhook endpoint
  • ✅ Webhook URL registered in MercadoPago dashboard
  • ✅ Signature validation enabled
  • ✅ Idempotency handling implemented
  • ✅ Webhook logs configured
  • ✅ Quick response time (< 5s)
  • ✅ Queue system for processing
  • ✅ Error monitoring alerts

Next steps

Processing Payments

Learn about payment creation

Refunds

Handle payment refunds

Testing

Test webhooks locally

Build docs developers (and LLMs) love