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'];

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).


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:

OutcomeRedirect targetThen redirects to
SuccessGET /api/v1/payments/paymongo/success?id={reference_number}{FRONTEND_URL}/donations/success?id={reference_number}
CancelGET /api/v1/payments/paymongo/cancel?id={reference_number}{FRONTEND_URL}/donations/cancelled

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 completed and sets paid_at
  • Increments the campaign's raised_amount, available_amount, and supporter_count by the base donation amount
  • Marks the campaign completed if raised_amount >= goal_amount

POST/api/v1/donations/{id}/pay/paymongo

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).

EventAction
checkout_session.payment.paidDonation → completed; increment campaign balances (primary completion signal for hosted checkout)
payment.paidDonation → completed if resolvable; otherwise logged and skipped
payment.failedDonation → 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:


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.

Was this page helpful?