Your first payment
In this tutorial you take a test-mode payment for an order from start to finish:
- The customer clicks Pay on their order, and your app creates a Checkout Session and sends them to PayMongo's hosted payment page.
- They pay with a card or e-wallet, and PayMongo sends them back to your order page.
- PayMongo calls your webhook with
checkout_session.payment.paid, and your app fulfils the order.
It takes three files: a controller, two routes, and a listener. Every snippet below is a real file that the package's test suite runs, so it works as written.
Before you start
- Install the package and add your test API keys, as in Installation & configuration.
- Your app has an
OrderEloquent model with a uniquereference(such asORDER-1234), adescription, an integeramountin centavos, and a nullablepaid_attimestamp. - Your app has an order page named
orders.showthat takes the order.
1. Send the customer to checkout
Create a Checkout Session for the order and redirect the customer to its checkoutUrl, a payment page PayMongo hosts for you:
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Luigel\Paymongo\Facades\Paymongo;
class CheckoutController
{
public function __invoke(Order $order): RedirectResponse
{
$session = Paymongo::checkoutSessions()->create([
'line_items' => [
[
'name' => $order->description,
'amount' => $order->amount, // integer centavos: 150050 = PHP 1,500.50
'currency' => 'PHP',
'quantity' => 1,
],
],
'payment_method_types' => ['card', 'gcash', 'paymaya'],
'reference_number' => $order->reference,
'success_url' => route('orders.show', $order),
'cancel_url' => route('orders.show', $order),
]);
return redirect()->away($session->checkoutUrl);
}
}
amountis integer centavos per unit, never a float: an order of PHP 1,500.50 is150050.payment_method_typeslists what the customer may pay with. Each must be enabled on your PayMongo account.reference_numberis your order's reference. It comes back on the webhook, which is how you will find the order again.- PayMongo sends the customer to
success_urlafter paying and tocancel_urlif they back out. Reachingsuccess_urlis not proof of payment, since anyone can open that URL. The webhook in step 3 is.
2. Add the routes
In routes/web.php, route the Pay button to the controller, and register the webhook endpoint PayMongo will call:
use App\Http\Controllers\CheckoutController;
use Illuminate\Support\Facades\Route;
Route::post('/orders/{order}/checkout', CheckoutController::class)->name('orders.checkout');
Route::paymongoWebhooks(); // POST /paymongo/webhook
Route::paymongoWebhooks() registers POST /paymongo/webhook. It verifies each delivery's signature, skips deliveries it has already handled, and dispatches a Laravel event for each PayMongo event. CSRF protection is turned off for it automatically.
The Pay button on your order page posts to the new route:
<form method="POST" action="{{ route('orders.checkout', $order) }}">
@csrf
<button type="submit">Pay</button>
</form>
3. Register the webhook endpoint
PayMongo needs a public URL to call. Locally, expose your app with a tunnel such as ngrok http 8000, then register the endpoint for the checkout_session.payment.paid event:
php artisan paymongo:webhook:create https://your-tunnel.ngrok.app/paymongo/webhook --event=checkout_session.payment.paid
The command prints the endpoint's secret_key. PayMongo shows it only once, so put it in .env straight away:
PAYMONGO_WEBHOOK_SECRET=whsk_...
4. Fulfil the order
When the customer pays, PayMongo calls your endpoint and the package dispatches Luigel\Paymongo\Events\CheckoutSessionPaymentPaid. Listen for it in app/Listeners/FulfillOrder.php:
namespace App\Listeners;
use App\Models\Order;
use Luigel\Paymongo\Events\CheckoutSessionPaymentPaid;
class FulfillOrder
{
public function handle(CheckoutSessionPaymentPaid $event): void
{
$order = Order::where('reference', $event->event->resourceAttribute('reference_number'))->firstOrFail();
$order->paid_at = now();
$order->save();
// Ship it, email a receipt, ...
}
}
- Laravel discovers the listener from the type hint on
handle(), so there is nothing to register. $event->event->resourceAttribute()reads a field of the checkout session PayMongo sent, by dot path.resourceId()is itscs_...id.- The package has already verified that PayMongo sent the event and dropped repeat deliveries of it, so the listener only does your part.
The webhook can arrive a moment after the customer lands back on your order page. Show "Payment processing" there until paid_at is set.
5. Try it
- Open an order and click Pay. You land on PayMongo's hosted checkout page.
- Pay by card with
4343434343434345, any future expiry date, and any CVC. Or choose GCash or Maya and click Authorize on PayMongo's test page. - PayMongo sends you back to the order page, and a moment later the order has a
paid_at.
Order still unpaid? Check that the tunnel is still running and that PAYMONGO_WEBHOOK_SECRET matches the endpoint: a mismatched secret makes your app answer 401, and the Webhooks page of the PayMongo dashboard lets you retry a failed delivery.
To cover this flow in your test suite without calling PayMongo, fake the API with Paymongo::fake() and post a signed event built by Fixtures::event(). See Testing.
Next steps
- Choose a flow: when a Checkout Session is the right fit, and when to use Payment Intents, Payment Links, or QR Ph instead.
- Checkout Sessions: line items, expiring a session, and reading its payments.
- Webhooks: every event, multiple endpoints, and signature details.