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:
Go to Your integrations → Webhooks
Create or edit a webhook configuration
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:
Extract signature components
Parse x-signature header for timestamp (ts) and signature hash (v1)
Build manifest
Construct manifest string: id:{resource_id};request-id:{request_id};ts:{timestamp};
Compute HMAC
Calculate HMAC-SHA256 of manifest using webhook secret
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
Type Description 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