Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mercadopago Laravel Package

puntodev/mercadopago

A Laravel/PHP integration for MercadoPago that simplifies creating payment preferences, handling checkout flows, and processing webhooks, giving developers a convenient wrapper for adding MercadoPago payments to their applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Check the platform constraint before anything else. composer.json requires PHP ^8.5 and illuminate/support ^13.0. On a typical Laravel 10/11/12 app Composer will simply refuse to install this. Confirm your runtime matches before you plan any work around it.

Then:

  1. composer require puntodev/mercadopago — package discovery registers MercadoPagoServiceProvider and the MercadoPago facade alias.
  2. Publish the config (php artisan vendor:publish and pick the package's tag/provider — check MercadoPagoServiceProvider::publishes()), then set in .env:
    • MERCADOPAGO_API_CLIENT_ID
    • MERCADOPAGO_API_CLIENT_SECRET
    • SANDBOX_GATEWAYS=true locally These are OAuth2 client credentials, not an access token. There is no MERCADOPAGO_ACCESS_TOKEN in this package.
  3. Read these three files in this order — they are the entire public surface:
    • src/MercadoPagoApi.php — five methods: createPaymentPreference, findMerchantOrders, findMerchantOrderById, findPayments, findPaymentById.
    • src/PaymentPreferenceBuilder.php + PaymentPreferenceItemBuilder.php — the only payload shapes you have to learn.
    • src/MercadoPagoClient.php — token caching and the usingSandbox() flag.

First use case: create a Checkout Pro preference and redirect the buyer.

use Puntodev\MercadoPago\Facades\MercadoPago;

$client = MercadoPago::defaultClient();          // reads client_id/secret from config

$preference = $client->createPaymentPreference(
    // PaymentPreferenceBuilder fluent setters: items, payer, back_urls,
    // notification_url, external_reference, payment_methods, expiration, binary_mode.
    // Confirm exact method names in the builder before writing the call.
    $builder->toArray()
);

$order->update(['mp_preference_id' => $preference['id']]);

$url = MercadoPago::usingSandbox()
    ? $preference['sandbox_init_point']
    : $preference['init_point'];

return redirect($url);

Note what is absent: there is no payment()->create(), no preference get(), no webhook controller, no signature validator. The description's "webhooks" and "payment handling" bullets describe MercadoPago's capabilities generally — you implement those pieces yourself, using findPaymentById to read state.

Implementation Patterns

Keep the facade out of controllers. Bind a MercadoPagoGateway in a service provider that takes the MercadoPago factory and exposes your domain methods (createCheckout(Order $order): string, syncPayment(string $id): ?Payment). Your controllers and jobs then speak in Order, not in SDK arrays, and you can swap the gateway for a fake in tests.

Persist the preference immediately — you can never fetch it back. createPaymentPreference is the only preference method. Store id, init_point, sandbox_init_point, and the payload you sent on the order row before redirecting. That stored payload is your only record if a dispute arises.

Use findPayments as your reconciliation workhorse. This is the pattern you will use daily:

$payments = $client->findPayments([
    'external_reference' => (string) $order->id,
    'sort'               => 'date_created',
    'criteria'           => 'desc',
]);

foreach ($payments['results'] ?? [] as $p) {
    if ($p['status'] === 'approved'
        && (float) $p['transaction_amount'] === (float) $order->total
        && $p['currency_id'] === $order->currency) {
        $order->markAsPaid($p['id']);
    }
}

findMerchantOrders / findMerchantOrderById complement this when one payment covers several order lines.

Webhook is yours to build. A route that is withoutMiddleware(VerifyCsrfToken::class), public but rate-limited:

  1. Validate x-signature (HMAC-SHA256 over the id:...;request-id:...;ts:...; manifest) with your MERCADOPAGO_WEBHOOK_SECRET — the package does not do this.
  2. Store the raw payload (PII-redacted) in a mercadopago_notifications table.
  3. Treat the payload's id as a hint: re-query with findPaymentById($id) and verify status, external_reference, amount, and currency before touching the order.
  4. Dispatch a queued job and return 200 fast — MercadoPago times out and retries.

Idempotency everywhere. Unique index on the MercadoPago payment id plus firstOrCreate (or DB::transaction + lockForUpdate) inside the job. Retried approved notifications are normal, and so are duplicate deliveries to your queue.

Never fulfil from back_urls. The success redirect is a UI hint only — the user can close the tab. Render "processing / confirmed" there based on your own order state.

Multi-tenant / multi-account. MercadoPagoClient is readonly; use MercadoPago::withCredentials($id, $secret) to get a new client per tenant instead of mutating config. That is Octane-safe and keeps tokens separate. Wrap it in a ClientResolver::forTenant($tenant).

Testing. The client uses Laravel's Http facade, so Http::fake() covers you — but remember to fake both POST /oauth/token and the API call, and flush the mercadopago-token-* cache key between tests (or use the array cache driver). Do not copy the package's own MercadoPagoApiTest approach: those tests make real HTTP calls against a real test account.

A nightly sweep pays for itself. Orders stuck in pending for >30 min: call findPayments(['external_reference' => $id]) and reconcile. Webhooks get lost; this catches them.

Gotchas and Tips

  • There is no transparent checkout here. No payment creation, no card tokenization, no bricks. This is Checkout Pro (redirect) only. For Checkout API you must drop to Http::withToken($token)->post('https://api.mercadopago.com/v1/payments', ...) yourself — the package won't get in your way, but it won't help either.
  • Sandbox is not a host switch. The base URL is always api.mercadopago.com; the environment is decided by which credentials you use. SANDBOX_GATEWAYS only flips the usingSandbox() flag so you can pick sandbox_init_point vs init_point. Mixing a production URL with test credentials produces the classic "payment not found".
  • Token cache staleness. getToken() does Cache::remember("mercadopago-token-{$clientId}", 1000, ...). After rotating credentials in production you have up to ~17 minutes of 401s. Run Cache::forget("mercadopago-token-{$oldClientId}") in your rotation path. If staging and production share a Redis/cache namespace and a client id, they will share tokens — namespace your cache store or prefix.
  • Every request uses ->throw(). Failures surface as Illuminate\Http\Client\RequestException. Catch it at the gateway boundary and log $e->response->body() — MercadoPago's error body explains the validation failure far better than the exception message.
  • auto_return is hardcoded to all. MercadoPago rejects a preference with auto_return but no valid back_urls. Also, pending and failure silently fall back to the success URL — a typo'd failure route won't error, it will just send everyone to the success page. Assert your back URLs in a test.
  • Default item currency is ARS. Selling in USD/BRL without overriding the currency on the item gets you rejections or mismatched totals. Check the item builder for the currency setter and always set it explicitly.
  • unit_price is rounded to two decimals by the builder. Pass 99.90, never integer cents. This is the single most common cause of a 100× charge.
  • excluded_payment_types is omitted entirely when empty, and the expiration block only appears when an expiration is set (formatted with MP_DATE_TIME_FORMAT). Passing [] to "reset" exclusions does nothing — build a fresh builder instead.
  • defaultClient() is a container singleton. It captures config at resolution. If you mutate config at runtime (Config::set()), the already-resolved singleton keeps the old credentials — the reason the withCredentials() per-call pattern is preferable, especially under Octane.
  • Extension point: don't fork. When you need an endpoint the wrapper lacks, either implement MercadoPagoApi for your own decorator, or call the REST API directly with the same OAuth token. The interfaces are the public contract, so a fake implementation also makes a clean test double.
  • Watch the package's test discipline and copy it. v7.0.5 was maintenance only (Dependabot + test-account provisioning instead of hardcoded IDs) — no source or config changes, so ^7.0 is safe and low-churn. Mirror the same approach in your own integration tests: look up or create fixtures dynamically rather than hardcoding MercadoPago IDs.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console