luigel/laravel-paymongo
Laravel integration for PayMongo, the Philippine payment gateway. Provides an expressive wrapper for payment intents, sources, checkout sessions, webhooks, and related APIs, with configuration, facades, and fluent methods to accept GCash, cards, and other payment methods.
This is the Laravel wrapper for PayMongo (Philippines payment gateway: cards, GCash, GrabPay, Maya, QR Ph, installments). v3.0.1 is a small but API-surface-breaking patch, so read the upgrade note before composer update.
composer require luigel/paymongophp artisan vendor:publish --tag=paymongo-config (or the provider's tag as documented)..env:
PAYMONGO_SECRET_KEY=sk_test_...
PAYMONGO_PUBLIC_KEY=pk_test_...
Keep test keys in .env.testing; the config reads them at boot.use Luigel\Paymongo\Facades\Paymongo;
$intent = Paymongo::paymentIntent()->create([
'amount' => 10000, // centavos
'currency' => 'PHP',
'payment_method_allowed' => ['card', 'gcash', 'paymaya'],
'description' => 'Order #1234',
]);
Then create a payment method client-side (or via Paymongo::paymentMethod()->create()), attach it, and redirect the user to the returned next_action.redirect.url.Look first at: the published config/paymongo.php, the Luigel\Paymongo\Facades\Paymongo facade for the resource list (paymentIntent, paymentMethod, source, checkout, link, webhook, refund, customer), and the package's webhook route/controller before you write your own.
If you only need hosted checkout, start with Paymongo::checkout()->create([...]) and skip intents entirely — it's the shortest path to a working flow.
Intent + attach + webhook (most control). Create the intent, attach the payment method, return next_action to the front end, and treat the webhook as the source of truth for fulfilment. Never mark an order paid from the browser redirect — it can be closed mid-3DS.
Checkout Sessions / Links (least code). Paymongo::checkout()->create() gives you a checkout_url and a checkout_session_id; store the id on your order and reconcile via webhook. Links are the same idea for reusable/amount-editable payment pages.
Sources for QR Ph / e-wallets. Older flows still use Paymongo::source()->create() then paymentIntent()->create() with the source id. Prefer intents if your integration is new.
Webhook handling. Register once via artisan/console, not on every request:
Paymongo::webhook()->create([
'url' => route('paymongo.webhook'),
'events' => [\Luigel\Paymongo\Enums\WebhookEventType::PAYMENT_PAID, /* ... */],
]);
Listen to the package events (e.g. PaymentPaid, PaymentFailed, SourceChargeable) or catch-all WebhookReceived. In v3.0.1 make WebhookReceived your primary hook if you were relying on any of the removed event classes — see Gotchas.
Idempotency + correlation. Store payment_intent_id / checkout_session_id on your orders table with a unique index. In the listener, look the order up by that id, not by amount or email. Wrap fulfilment in a transaction and a DB::transaction + wasRecentlyCreated/already_processed guard: PayMongo retries webhooks.
Queue the listeners. implements ShouldQueue on your listeners so the webhook endpoint returns 200 fast; PayMongo times out and retries otherwise.
Testing. Fake the HTTP layer (the package uses Laravel's HTTP client) — Http::fake([...]) with fixture payloads from PayMongo's docs is more reliable than faking the facade, because you also exercise signature/payload parsing. Add one test per event you actually listen to.
WebhookEventType cases and their matching Luigel\Paymongo\Events\* classes are gone: payment_intent.succeeded, payment_intent.awaiting_payment_method, subscription.activated, qr.paid, qr.expired, refund.succeeded, dispute.created, dispute.resolved, payout.deposited, payout.returned. If you used any of those classes, or matched on those strings in a switch, you'll get a fatal error / dead branch after upgrading. Grep for the strings before you bump:
grep -rn "payment_intent.succeeded\|qr.paid\|refund.succeeded\|dispute.resolved\|payout\." app/ config/ tests/
They never fired in practice, so removal is safe at runtime — the risk is purely class not found in your own code and stale array keys in a hard-coded webhook events list."luigel/paymongo": "^3.0.1" and commit the lockfile; don't float * in a payments codebase.WebhookReceived for names it doesn't recognise (including the ten above, if PayMongo ever starts sending them). Route critical logic through WebhookReceived + your own event-name switch if you want future-proofing; use the typed events for convenience only.parameter_invalid if you passed unsupported event types. If your php artisan paymongo:webhook (or manual POST /v1/webhooks) call is still 400ing, you're either on an older version or still passing a stale event list — the API rejects the whole request, not just the bad entry.Paymongo-Signature (contains t= and te=/li= HMACs). Verify against the raw body — $request->all() re-encoded will not match. Exclude the webhook route from CSRF (VerifyCsrfToken::$except or bootstrap/app.php for Laravel 11+).10000 = ₱100.00. Rounding bugs here are silent and expensive.parameter_invalid from PayMongo rather than a clean form error.next_action is not always present. For methods that complete inline (paid status immediately), skip the redirect; branch on $intent->status and $intent->next_action.sk_live_ in a local .env will move real money. Guard with a config check in non-production.status fetched from the API (Paymongo::paymentIntent()->find($id)) rather than trusting event order for terminal states.Http::fake() and a global middleware (logging request ids from Paymongo-Request-Id headers) are your debugging tools when support asks for a trace.How can I help you explore Laravel packages today?