mongosprout/paymongo-bridge

PayMongo API client and webhook pipeline for Laravel. Brings the plumbing, imposes no billing model.

Maintainers

Package info

github.com/mongosprout/paymongo-bridge

pkg:composer/mongosprout/paymongo-bridge

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-03 02:54 UTC

This package is auto-updated.

Last update: 2026-08-03 03:01:20 UTC


README

PayMongo API client and webhook pipeline for Laravel. Brings the plumbing, imposes no billing model.

The package knows how to talk to PayMongo and how to receive a webhook safely. It does not know what a subscription means in your application, so it ships no billing schema and only one table of its own.

Install

composer require mongosprout/paymongo-bridge
PAYMONGO_SECRET_KEY=sk_test_...
PAYMONGO_PUBLIC_KEY=pk_test_...
PAYMONGO_WEBHOOK_SECRET=whsk_...

Live or test mode is read from the secret key's prefix, never configured, so the two can never disagree.

Collecting money

use MongoSprout\PayMongo\Facades\PayMongo;
use MongoSprout\PayMongo\Money;

$customer = PayMongo::customers()->create([
    'first_name' => 'Maria', 'last_name' => 'Santos',
    'email' => 'maria@example.com', 'phone' => '+639171234567',
]);

// Hosted checkout, for the wallets that cannot be auto debited.
$session = PayMongo::checkoutSessions()->create([
    'line_items' => [[
        'name' => 'Business plan',
        'amount' => Money::ofMajor('2000.00')->toCentavos(),
        'currency' => 'PHP',
        'quantity' => 1,
    ]],
    'payment_method_types' => ['gcash', 'card', 'paymaya', 'grab_pay'],
    'reference_number' => $invoice->number,
    'metadata' => ['invoice_id' => (string) $invoice->id],
    'success_url' => route('billing.return', $invoice),
    'cancel_url' => route('billing.cancel', $invoice),
]);

return redirect($session->attr('checkout_url'));

Recurring billing works for cards and Maya only. Vault the card on the customer first, then subscribe:

PayMongo::paymentMethods()->attachToCustomer($customerId, $paymentMethodId);
PayMongo::subscriptions()->create($customerId, $planId, ['invoice_id' => (string) $invoice->id]);

Receiving webhooks

Register the endpoint once, and store the secret it prints:

php artisan paymongo:webhook:register https://your-app.test/paymongo/webhook

PAYMONGO_WEBHOOK_SECRET accepts several secrets, comma separated. Each registered endpoint has its own and re-registering mints a new one, so a URL move or a rotation lists old and new together, then drops the old once its endpoint is disabled. A delivery verifies if any listed secret signs it.

Then listen. Every delivery is signature checked, deduplicated and queued before your listener runs.

use MongoSprout\PayMongo\Webhooks\Events\SubscriptionInvoicePaid;

class GrantAccess
{
    public function handle(SubscriptionInvoicePaid $event): void
    {
        $invoice = Invoice::find($event->metadata('invoice_id'));

        // Handlers must be safe to run twice. PayMongo redelivers.
        $invoice?->markPaid($event->resource()->centavos());
    }
}

The endpoint authenticates by signature, not by middleware, but consider adding 'throttle:60,1' to paymongo.webhook.middleware so junk traffic cannot fill your log with rejections.

Deliveries are stored in paymongo_webhook_events, full payload included, and the table grows forever. Prune it on whatever schedule suits you — but never delete rows younger than the max_age window (3 days by default). Signatures older than max_age are refused outright, so those rows are free to go; inside the window the unique event id is what stops a captured delivery from being replayed.

Sending money

Off by default. An application that only collects payments should not carry a payout surface:

PAYMONGO_DISBURSEMENTS_ENABLED=true
$transfers = PayMongo::transfers();

$transfers->createBatch([
    $transfers->transfer(
        sourceNumber: config('paymongo.disbursements.source_account_number'),
        sourceName: config('paymongo.disbursements.source_account_name'),
        destinationNumber: $employee->ewallet_number,
        destinationName: $employee->full_name,
        destinationBic: 'GXCHPHM2',
        amount: Money::ofMajor($item->net_pay),
        referenceNumber: $item->reference_number,
    ),
]);

Items in a batch settle independently, so reconcile per transfer rather than per batch.

Money

PayMongo speaks integer centavos. Applications often do not. Money converts across that boundary on the decimal string rather than on a float, so 0.29 cannot quietly become 28 centavos the way (int) (0.29 * 100) does.

Money::ofMajor('1500.15')->toCentavos();   // 150015
Money::ofCentavos(150015)->toMajor();      // "1500.15"  safe for a decimal column

Testing

InteractsWithPayMongo is a plain trait, so it works in Pest and in PHPUnit.

$this->fakePayMongo();

$this->paymongoWebhook('subscription.invoice.paid', metadata: ['invoice_id' => '7'])->assertOk();

The webhook helper signs the exact bytes it sends. Posting through postJson() would re-encode the body and the signature would never match.