Laravel PayMongo

Upgrading from v2

v3 is a rewrite. The v2 facade's fluent, stateful modules with magic getters are gone. In their place are one service per resource, typed read-only data objects, Laravel's HTTP client with automatic idempotency keys and retries, a webhook route that dispatches Laravel events, and fakes for your tests.

Three ideas drive the breaking changes:

  • Exact amounts. You send the integer centavos PayMongo speaks. Nothing is converted from floats behind your back.
  • Types, not magic. Read-only properties and enums instead of __get and get*(), so your IDE and static analysis see everything.
  • Laravel-native. The Http client underneath, so it can be faked; events for webhooks; a route macro; current publish and command conventions.

Requirements

v2 v3
PHP 8.2+ 8.2+ (Laravel 13 requires 8.3+)
Laravel 10 to 13 11 to 13
composer require luigel/laravel-paymongo:^3.0

At a glance

Area v2 v3
Amounts float pesos, converted for you (1500.50) integer centavos, sent as they are (150050)
Facade modules: Paymongo::paymentIntent()->create() services: Paymongo::paymentIntents()->create()
Responses magic getters (getStatus(), getBillingName()) typed properties and enums (->status, ->billing?->name)
Lists ->all() returns a Collection ->list() returns a CursorPage
Exceptions BadRequestException, UnauthorizedException, ... subclasses of PaymongoException, with the parsed PayMongo errors
Config config/config.php, published as paymongo.php config/paymongo.php, in a new shape
Webhook verification paymongo.signature:{event}, a secret per event paymongo.signature[:{name}], a secret per endpoint
Receiving webhooks your own controller Route::paymongoWebhooks(), Laravel events, deduplication
Artisan commands paymongo:webhook, paymongo:list-webhooks, paymongo:toggle-webhook paymongo:webhook:create, paymongo:webhook:list, paymongo:webhook:toggle
Tokens API deprecated removed
Sources API supported deprecated, still available

1. Send amounts in centavos

v2 took float pesos and multiplied them by 100 (the amount_type config). v3 sends amounts exactly as you pass them, so pass integer centavos, as the PayMongo API expects. Amounts in responses are integer centavos too:

use Luigel\Paymongo\Facades\Paymongo;

$link = Paymongo::links()->create([
    'amount' => 150050, // v2 took 1500.50
    'description' => 'Invoice #1234',
]);

$link->amount;               // 150050, where v2's getAmount() returned 1500.5
$link->money()?->format();   // "₱1,500.50"
$link->money()?->toDecimal(); // "1500.50"

Check every amount you send, including checkout line_items[].amount: multiply pesos by 100 and cast to int. See Amounts & Money. The amount_type config key, the Paymongo::AMOUNT_TYPE_FLOAT and Paymongo::AMOUNT_TYPE_INT constants, and AmountTypeNotSupportedException are gone.

2. Call the services

Each resource has a service on the facade, and methods take the id instead of acting on a model you find()-ed first.

v2 v3
Paymongo::paymentIntent()->create($payload) Paymongo::paymentIntents()->create($attributes)
Paymongo::paymentIntent()->find($id) Paymongo::paymentIntents()->retrieve($id)
$intent->attach($paymentMethodId, $returnUrl) Paymongo::paymentIntents()->attach($id, $paymentMethodId, $returnUrl)
$intent->cancel() Paymongo::paymentIntents()->cancel($id)
none Paymongo::paymentIntents()->capture($id, $amount)
none Paymongo::paymentIntents()->retrieveUsingClientKey($id, $clientKey)
Paymongo::paymentMethod()->create($payload) Paymongo::paymentMethods()->create($attributes)
Paymongo::paymentMethod()->find($id) Paymongo::paymentMethods()->retrieve($id)
Paymongo::payment()->create($payload) removed: PayMongo creates the payment when a payment intent, checkout session or link is paid
Paymongo::payment()->find($id) Paymongo::payments()->retrieve($id)
Paymongo::payment()->all() Paymongo::payments()->list($params)
Paymongo::refund()->create($payload) Paymongo::refunds()->create($attributes)
Paymongo::refund()->find($id) Paymongo::refunds()->retrieve($id)
Paymongo::refund()->all() Paymongo::refunds()->list($params)
Paymongo::webhook()->create(['url' => $url, 'events' => $events]) Paymongo::webhooks()->create($url, $events)
Paymongo::webhook()->find($id) Paymongo::webhooks()->retrieve($id)
Paymongo::webhook()->all() Paymongo::webhooks()->list()
$webhook->update($payload) Paymongo::webhooks()->update($id, $attributes)
$webhook->enable(), $webhook->disable() Paymongo::webhooks()->enable($id), disable($id)
Paymongo::link()->create($payload) Paymongo::links()->create($attributes)
Paymongo::link()->find($id) Paymongo::links()->retrieve($id)
Paymongo::link()->find($referenceNumber) Paymongo::links()->retrieveByReference($referenceNumber)
none Paymongo::links()->list($params)
$link->archive(), $link->unarchive() Paymongo::links()->archive($id), unarchive($id)
Paymongo::customer()->create($payload) Paymongo::customers()->create($attributes)
Paymongo::customer()->find($id) Paymongo::customers()->retrieve($id)
$customer->update($payload) Paymongo::customers()->update($id, $attributes)
$customer->delete() Paymongo::customers()->delete($id)
$customer->paymentMethods() Paymongo::customers()->paymentMethods($customerId)
none Paymongo::customers()->deletePaymentMethod($customerId, $paymentMethodId)
Paymongo::checkout()->create($payload) Paymongo::checkoutSessions()->create($attributes)
Paymongo::checkout()->find($id) Paymongo::checkoutSessions()->retrieve($id)
$checkout->expire() Paymongo::checkoutSessions()->expire($id)
Paymongo::source()->create($payload) Paymongo::sources()->create($attributes), deprecated
Paymongo::source()->find($id) Paymongo::sources()->retrieve($id), deprecated
Paymongo::token()->create($payload), find($id) removed

New in v3, with no v2 counterpart: Payment Links, QR Ph, Payouts, Plans & Subscriptions, and Paymongo::withSecretKey() for more than one account.

3. Read typed properties

v2's getData(), __get and get*() methods are gone. v3 returns data objects from Luigel\Paymongo\Data with read-only typed properties, and fields with a fixed set of values, such as statuses, are enums:

use Luigel\Paymongo\Enums\PaymentIntentStatus;
use Luigel\Paymongo\Facades\Paymongo;

$intent = Paymongo::paymentIntents()->retrieve('pi_hsJNpsRFU1LxgVbxW4YJHRs6'); // v2: ->find()

$intent->status === PaymentIntentStatus::Succeeded; // v2: getStatus() === 'succeeded'
$intent->status?->value;                            // the raw string, when you need one
$intent->clientKey;                                 // v2: getClientKey()
$intent->attribute('payment_method_options.card.request_three_d_secure'); // any raw attribute
$intent->createdAt();                               // ?CarbonImmutable, not a unix int
$intent->toArray();                                 // v2: getData()

A value the package does not know yet, such as a status PayMongo added after this release, reads as null. ->attribute('status') always has the raw string. The Data objects reference lists every property.

4. Catch the new exceptions

Every exception extends Luigel\Paymongo\Exceptions\PaymongoException, which carries the HTTP ->status and PayMongo's parsed errors, ->errors() and ->firstError(). See Errors.

v2 v3
BadRequestException InvalidRequestException (400, 403, 422 and any other 4xx)
UnauthorizedException AuthenticationException (401)
PaymentErrorException PaymentDeclinedException (402)
NotFoundException ResourceNotFoundException (404)
MethodNotFoundException removed: there are no magic calls to mistype
AmountTypeNotSupportedException removed: amounts are always centavos
none RateLimitException (429), ServerException (5xx), ConnectionException (PayMongo unreachable)
none InvalidWebhookSignatureException (a webhook that fails verification)

5. Republish the config

The config file moved from config/config.php to config/paymongo.php, and its publish tag changed. Delete your published config/paymongo.php and publish it again:

php artisan vendor:publish --tag=paymongo-config
v2 key v3
secret_key, public_key, livemode unchanged
version removed: the API version header is no longer sent
amount_type removed: amounts are always centavos
signer, signature_header_name removed: verification is built in, and always reads Paymongo-Signature
webhook_signature, webhook_signatures.{event} a secret per endpoint: webhooks.secret, and webhooks.secrets.{name} for more
none base_url, http.timeout, http.retries, http.retry_delay, idempotency.auto
none webhooks.tolerance, webhooks.dedupe.enabled, webhooks.dedupe.ttl, webhooks.dedupe.store

Installation & configuration lists their environment variables. Rename your webhook secrets, from one per event to one per endpoint:

# v2
PAYMONGO_WEBHOOK_SIG=whsk_...
PAYMONGO_WEBHOOK_SIG_PAYMENT_PAID=whsk_...

# v3
PAYMONGO_WEBHOOK_SECRET=whsk_...

6. Receive webhooks with one route

The middleware alias is still paymongo.signature, now Luigel\Paymongo\Http\Middleware\VerifyWebhookSignature, but its parameter means something else. In v2 it named an event, such as paymongo.signature:payment_paid, and you had a route and a controller action per event. In v3 it names an endpoint's secret. A PayMongo endpoint has one secret for all the events it receives, so most apps need one route, with no parameter:

use Illuminate\Support\Facades\Route;

Route::paymongoWebhooks(); // POST /paymongo/webhook, named paymongo.webhooks

A second endpoint, with a secret of its own, gets a second route; see More than one endpoint.

Move each v2 controller action into a listener for its event, such as Luigel\Paymongo\Events\PaymentPaid or PaymentFailed. The route verifies each delivery and drops repeats before your listener runs. See Webhooks.

7. Rename the artisan commands

v2 v3
paymongo:webhook paymongo:webhook:create {url} {--event=*}
paymongo:list-webhooks paymongo:webhook:list
paymongo:toggle-webhook {id} --enable/--disable paymongo:webhook:toggle {id} --enable/--disable

paymongo:webhook:create no longer prompts. Pass the URL and events: php artisan paymongo:webhook:create https://example.com/paymongo/webhook --event=payment.paid --event=payment.failed.

8. Move off Tokens and Sources

PayMongo removed the Tokens API, and Paymongo::token() is gone with it. Card details become a payment method instead.

The Sources API still works but PayMongo has deprecated it. v3 still charges a chargeable source with payments()->create() (see Sources), but move GCash and GrabPay to a payment intent with an e-wallet payment method. PayMongo creates the payment when the customer authorizes it, so there is no step for you to charge:

use Luigel\Paymongo\Facades\Paymongo;

$intent = Paymongo::paymentIntents()->create([
    'amount' => 150050,
    'currency' => 'PHP',
    'payment_method_allowed' => ['gcash'],
]);

$method = Paymongo::paymentMethods()->create(['type' => 'gcash']);

$intent = Paymongo::paymentIntents()->attach($intent->id, $method->id, returnUrl: 'https://example.com/orders/1234');

$authorizeUrl = $intent->nextAction?->url; // redirect the customer here, then listen for payment.paid

Listen for payment.paid (PaymentPaid) instead of source.chargeable. See Sources and Payment Intents.

9. Rewrite your tests

Tests that mocked Guzzle or the Paymongo class need rewriting. Paymongo::fake() answers every request without the network, Paymongo::assertSent() and assertNothingSent() check what was sent, and Fixtures builds any payload, webhook events included. See Testing.