Laravel PayMongo

Multiple accounts

The Paymongo facade uses the account whose secret key is in PAYMONGO_SECRET_KEY. When one app takes payments for several PayMongo accounts, such as a marketplace where each merchant has their own, call Paymongo::withSecretKey() for a copy that uses another account's key:

use Luigel\Paymongo\Facades\Paymongo;

$merchantSecretKey = 'sk_test_merchant_account'; // e.g. decrypt($merchant->paymongo_secret_key)

$paymongo = Paymongo::withSecretKey($merchantSecretKey);

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

// Paymongo::paymentIntents() still uses PAYMONGO_SECRET_KEY.
  • withSecretKey() returns a new, separate PaymongoManager with every service, paymentIntents() through payouts(). Pass that account's public key as the optional publicKey: argument if you use retrieveUsingClientKey(); without it, the copy has no public key. The timeout, retries and idempotency settings still come from your config.
  • The facade itself is unchanged, so one merchant's key never leaks into a request made for another. Keep the copy in a variable for as long as you work with that account.
  • Store each merchant's secret key encrypted, for example with Laravel's encrypted cast.
  • Paymongo::fake() fakes the copies too, and Paymongo::assertSent() sees their requests. Check which account a request was made for with its Authorization header, which is HTTP Basic auth with the secret key as the username.

Webhooks for several accounts

Each account registers its own webhook endpoints, and each endpoint has its own secret. Register them with the copy for that account, for example Paymongo::withSecretKey($key)->webhooks()->create(...), then give each endpoint a route with its own named secret. See More than one endpoint.

A named secret lives in config/paymongo.php, so this suits a handful of accounts known up front. For accounts that come and go, register a route of your own that finds the account from the URL, and check each delivery against that account's secret with Luigel\Paymongo\Webhooks\SignatureVerifier:

use App\Models\Merchant;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Luigel\Paymongo\Events\WebhookReceived;
use Luigel\Paymongo\Exceptions\InvalidWebhookSignatureException;
use Luigel\Paymongo\Webhooks\SignatureVerifier;
use Luigel\Paymongo\Webhooks\WebhookEvent;

// routes/api.php: register each merchant's endpoint as https://example.com/webhooks/paymongo/{merchant id}
Route::post('webhooks/paymongo/{merchant}', function (Request $request, Merchant $merchant) {
    try {
        (new SignatureVerifier(tolerance: 300))->verify(
            $request->getContent(),
            $request->header('Paymongo-Signature'),
            $merchant->paymongo_webhook_secret,
            livemode: (bool) config('paymongo.livemode'),
        );
    } catch (InvalidWebhookSignatureException) {
        abort(401);
    }

    event(new WebhookReceived(WebhookEvent::fromArray($request->all())));

    return response()->json(['received' => true]);
});

verify() throws an InvalidWebhookSignatureException for a delivery that fails the checks described under Signature verification. Dispatching WebhookReceived hands the event to the same listeners as Route::paymongoWebhooks(). This route does not drop repeat deliveries, so make those listeners idempotent.