PayMongo Checkout
How PayMongo hosted checkout sessions are created, how the user is redirected, and how the result is confirmed with PayMongo's API.
Initiated via POST /api/v1/donations with payment_gateway: "paymongo". PayMongo sits alongside Maya and follows the same per-gateway architecture — API client → transaction service → controller → webhook middleware → routes.
Source files: app/Services/PayMongoService.php · app/Services/PayMongoTransactionService.php · app/Http/Controllers/Api/PayMongoController.php
Supported Payment Methods
The hosted checkout offers all methods enabled on the merchant account:
// app/Services/PayMongoTransactionService.php
private const CHECKOUT_PAYMENT_METHODS = ['card', 'gcash', 'paymaya', 'grab_pay', 'qrph'];
PayMongo only renders the methods that are activated on your PayMongo account. A method listed here that is not enabled in the dashboard simply won't appear on the checkout page.
Creating a PayMongo Checkout Session
Endpoint: POST /api/v1/donations
- Name
campaign_id- Type
- integer
- Description
Campaign to donate to
- Name
amount- Type
- number
- Description
Base donation amount in PHP (minimum: 1)
- Name
payment_gateway- Type
- string
- Description
Must be
"paymongo"for the PayMongo checkout path
- Name
is_anonymous- Type
- boolean
- Description
Default:
false
- Name
message- Type
- string
- Description
Optional message to the campaign (max 500 characters)
- Name
donor_name- Type
- string
- Description
Required for non-anonymous guest donations
- Name
donor_email- Type
- string
- Description
Required for non-anonymous guest donations
Request
{
"campaign_id": 1,
"amount": 1000,
"payment_gateway": "paymongo"
}
Response
{
"success": true,
"data": {
"redirectUrl": "https://checkout.paymongo.com/cs_abc123",
"transaction_id": "cs_abc123",
"reference_number": "550e8400-e29b-41d4-a716-446655440000"
},
"message": "Payment session created successfully"
}
The backend creates a pending Donation with a UUID reference_number, then opens a PayMongo checkout session and stores the session id (cs_...) in transaction_id. The frontend redirects the browser to redirectUrl (PayMongo's checkout_url).
Amounts sent to PayMongo are integer centavos — (int) round($donation->total_amount * 100). A ₱1,000.00 total is sent as 100000.
Internal Implementation
// app/Services/PayMongoTransactionService.php
private function executeCheckout(Donation $donation): array
{
$session = $this->paymongo->createCheckoutSession([
'line_items' => [[
'currency' => 'PHP',
'amount' => (int) round($donation->total_amount * 100), // centavos
'name' => 'Donation — '.($campaign->title ?? 'Campaign'),
'quantity' => 1,
]],
'payment_method_types' => self::CHECKOUT_PAYMENT_METHODS,
'success_url' => config('app.url').'/api/v1/payments/paymongo/success?id='.urlencode($donation->reference_number),
'cancel_url' => config('app.url').'/api/v1/payments/paymongo/cancel?id='.urlencode($donation->reference_number),
'reference_number' => $donation->reference_number,
'metadata' => ['donation_reference' => $donation->reference_number],
'send_email_receipt' => false,
]);
$donation->update(['transaction_id' => $session['id']]);
return [
'redirectUrl' => $session['attributes']['checkout_url'] ?? null,
'transaction_id' => $session['id'],
'reference_number' => $donation->reference_number,
];
}
PayMongoService wraps every request in PayMongo's envelope ({ data: { attributes: {...} } }) and authenticates with HTTP Basic auth using the secret key as the username and an empty password.
PayMongo Redirects
After the checkout, PayMongo redirects the browser back to the Batchmates backend:
| Outcome | Redirect target | Then redirects to |
|---|---|---|
| Success | GET /api/v1/payments/paymongo/success?id={reference_number} | {FRONTEND_URL}/donations/success?id={reference_number} |
| Cancel | GET /api/v1/payments/paymongo/cancel?id={reference_number} | {FRONTEND_URL}/donations/cancelled |
The success redirect calls verifyAndComplete() — it re-fetches the checkout session (or payment intent) from PayMongo's API and only marks the donation completed if PayMongo confirms payment succeeded. A redirect alone never completes a donation. The webhook is the primary completion signal; the redirect verify is a fallback for when the webhook is delayed.
The cancel handler marks a still-pending donation as cancelled before redirecting.
API-Verified Completion
verifyAndComplete() is idempotent and handles both checkout sessions (cs_...) and payment intents (pi_...):
// app/Services/PayMongoTransactionService.php
public function verifyAndComplete(string $referenceNumber): void
{
$donation = Donation::where('reference_number', $referenceNumber)->first();
if (! $donation || $donation->status === 'completed' || ! $donation->transaction_id) {
return;
}
if (str_starts_with($donation->transaction_id, 'pi_')) {
$intent = $this->paymongo->getPaymentIntent($donation->transaction_id);
if (($intent['attributes']['status'] ?? null) === 'succeeded') {
$this->completeDonation($donation, /* payment method type */);
}
return;
}
$session = $this->paymongo->getCheckoutSession($donation->transaction_id);
if ($this->checkoutSessionIsPaid($session)) {
$this->completeDonation($donation, $this->resolvePaymentMethodFromSession($session));
}
}
completeDonation() runs inside a DB transaction with lockForUpdate(). It re-checks that the donation is not already completed (so the webhook and the redirect verify can't double-count), then:
- Marks the donation
completedand setspaid_at - Increments the campaign's
raised_amount,available_amount, andsupporter_countby the base donation amount - Marks the campaign
completedifraised_amount >= goal_amount
Retrying a Failed Donation
Re-initiates a PayMongo checkout for an existing donation whose status is pending, failed, or expired. Returns a fresh redirectUrl. A completed donation cannot be retried.
Authentication: Required (donation owner, or a system_admin / institution_admin)
Rate limit: 5 requests per minute
For a brand-new donation use POST /donations with payment_gateway=paymongo instead.
Response
{
"redirectUrl": "https://checkout.paymongo.com/cs_def456",
"transaction_id": "cs_def456",
"reference_number": "550e8400-e29b-41d4-a716-446655440000"
}
Webhook
PayMongo fires server-to-server events to POST /api/v1/payments/paymongo/webhook, protected by the paymongo.webhook middleware (VerifyPayMongoWebhook).
| Event | Action |
|---|---|
checkout_session.payment.paid | Donation → completed; increment campaign balances (primary completion signal for hosted checkout) |
payment.paid | Donation → completed if resolvable; otherwise logged and skipped |
payment.failed | Donation → failed |
See Payment Webhooks for signature verification, the Paymongo-Signature header format, and the replay window.
Frontend Redirect Host Allowlist
Before sending the browser to redirectUrl, Donate.tsx validates the destination host against a per-gateway allowlist:
// resources/js/pages/donor/Donate.tsx
const GATEWAY_REDIRECT_HOSTS = {
paymongo: ['paymongo.com', 'checkout.paymongo.com', 'pm.link'],
// ...maya
}
An unexpected redirect host throws before navigation, guarding against an open-redirect via a tampered response.
Mobile / Native Clients
The REST contract is platform-neutral — native iOS/Android apps call the same POST /api/v1/donations endpoint and receive the same redirectUrl. The browser-specific pieces map to mobile as follows:
- Open
redirectUrlin an in-app browser —ASWebAuthenticationSession/SFSafariViewControlleron iOS, Chrome Custom Tabs on Android — not a raw embeddedWebView. Card 3DS and bank pages frequently block embedded WebViews. - The
success_url/cancel_urlresolve to the webFRONTEND_URL. For apps, register those return paths as deep links / universal links (or detect the return URL inside the auth session and dismiss it) so the app regains control after payment. - The webhook is the source of truth. After the browser session closes, fetch the donation (
GET /api/v1/donations/{id}) to read the final status — don't infer success from the redirect alone. - The redirect-host allowlist above is a web concern; the mobile equivalent is validating the URL host before handing it to the in-app browser.
Test Mode
Set PAYMONGO_LIVEMODE=false and use PayMongo test keys (pk_test_… / sk_test_…). Use PayMongo's published test cards to exercise success, failure, and 3DS scenarios. Set PAYMONGO_LIVEMODE=true in production so the live (li) webhook signature is verified.