lloricode/laravel-paymaya-sdk
Laravel PayMaya SDK provides a Laravel-friendly wrapper around PayMaya’s PHP SDK, making it easier to configure credentials and integrate PayMaya checkout, payment links, and related payment APIs into your Laravel application.
Install and publish config
composer require lloricode/laravel-paymaya-sdk
php artisan vendor:publish --provider="Lloricode\Paymaya\PaymayaServiceProvider"
Add the credentials to .env (names should match config/paymaya.php — verify after publishing):
PAYMAYA_PUBLIC_KEY=pk-xxxxxxxx
PAYMAYA_SECRET_KEY=sk-xxxxxxxx
PAYMAYA_ENVIRONMENT=sandbox # or production
Grab sandbox keys from the PayMaya/Maya developer dashboard. Sandbox and production keys are completely separate; a mismatch produces confusing auth errors rather than a clear "wrong environment" message.
Where to look first
config/paymaya.php — the authoritative list of env keys, base URLs, and any timeout/retry settings.vendor/lloricode/laravel-paymaya-sdk/src/ — confirm the facade class, the service provider binding, and the method names on the client. This package is thin and lightly documented, so the source is the documentation.Lloricode\Paymaya\Facades\Paymaya) — this is your entry point for checkout, payments, and customers.First use case: create a checkout and redirect
use Lloricode\Paymaya\Facades\Paymaya;
$checkout = Paymaya::checkout()->create([
'totalAmount' => ['value' => '100.00', 'currency' => 'PHP'],
'buyer' => ['firstName' => 'Juan', 'lastName' => 'Dela Cruz'],
'items' => [[
'name' => 'Order #123',
'quantity' => 1,
'amount' => ['value' => '100.00', 'currency' => 'PHP'],
'totalAmount' => ['value' => '100.00', 'currency' => 'PHP'],
]],
'redirectUrl' => [
'success' => route('checkout.success'),
'failure' => route('checkout.failure'),
'cancel' => route('checkout.cancel'),
],
'requestReferenceNumber' => (string) $order->id,
]);
return redirect($checkout['redirectUrl']);
Persist $checkout['checkoutId'] and the reference number before redirecting — you need them to reconcile the return trip and any webhook.
Wrap the SDK in a domain action, not in controllers. The SDK returns raw arrays, and payment payloads are wide. Put payload construction in a single CreateCheckoutAction / PaymayaCheckoutService class so the shape lives in one place and controllers stay thin:
final class CreateCheckout
{
public function __invoke(Order $order): string
{
$checkout = Paymaya::checkout()->create($this->payloadFor($order));
$order->payments()->create([
'checkout_id' => $checkout['checkoutId'],
'reference_no' => $order->id,
'status' => 'pending',
]);
return $checkout['redirectUrl'];
}
}
Always verify status server-side on the success callback. The success redirect carries a checkoutId query parameter, but the buyer controls that URL. Retrieve the checkout/payment from the API and confirm the amount and status before marking an order paid. Treat the redirect as a UX signal, not proof of payment.
Run webhook + redirect as idempotent paths into the same state machine. PayMaya will hit your webhook and redirect the buyer; they race. Both should call the same markPaid(Order, PaymentRef) method guarded by a DB transaction or updateOrCreate on the payment reference so double-processing is harmless.
Route::post('/webhooks/paymaya', PaymayaWebhookController::class)
->withoutMiddleware(['csrf']);
Poll or query for status when you have no webhook yet. During local development, hitting checkout/{id} or the payment endpoint on the success page is the fastest loop. Only move to webhooks once URLs are publicly reachable.
Design for multiple accounts if you're multi-tenant. A facade backed by a singleton reads config once. If different tenants have different merchant accounts, resolve a fresh client per tenant (e.g. app(PaymayaClient::class, ['config' => $tenantConfig]) if the package supports constructor config, otherwise config([...]) before resolving and forget the singleton) rather than relying on global config.
Test with the Laravel HTTP client when possible. If the SDK is built on Illuminate\Support\Facades\Http, this is enough:
Http::fake([
'*/checkouts' => Http::response(['checkoutId' => 'abc', 'redirectUrl' => 'https://pg.test/x'], 200),
]);
$this->post('/checkout', ...)->assertRedirect('https://pg.test/x');
If it uses Guzzle directly, mock the underlying client class instead — but still assert on the outgoing payload; most integration bugs are wrong field names or amount formatting, not transport failures.
Model the payloads as DTOs. Amounts, buyer info, and item arrays are easy to mistype. A small CheckoutPayload::toArray() avoids the "works in Postman, fails in PHP" class of bug.
number_format($cents / 100, 2, '.', '') and always emit two decimals ("100.00", not "100"). Sending an integer amount is a common 400.requestReferenceNumber must be unique per checkout. Reusing an order ID after a failed/cancelled attempt will be rejected or collide when reconciling. Append an attempt suffix ("{$order->id}-2") and store the mapping.php artisan config:cache freezes the facade's config. Any runtime config(['paymaya.secret_key' => ...]) will silently do nothing until you re-resolve the client. Prefer per-request client instances over mutating config.localhost and http:// fail in the sandbox; use ngrok/Expose or a staging host. Verify the success URL actually comes back with checkoutId before debugging anything else.php://input before any middleware touches it, compare with hash_equals, and return 200 fast — a slow handler causes retries and duplicate deliveries.totalAmount.value from the API response against your order total; a mismatch means tampering or a stale pending checkout.try/catch for connection/timeout errors. Payment endpoints can be slow; set a client timeout and surface a retryable error to the user rather than a 500. Distinguish transport errors from 4xx validation errors (usually a payload shape problem) and 401s (key/environment mismatch).Authorization headers and buyer details; never log secret_key.composer.json, check the composer.json require block for the Laravel/PHP versions it actually supports, and be prepared to vendor the relevant client class or bypass it with direct HTTP calls if it lags behind API changes. The underlying PayMaya REST API is stable; the wrapper is the fragile part.vendor/. Either extend the client class and rebind it in AppServiceProvider, or call the API directly with Http::withToken(...). Keep the wrapper as a convenience, not a hard dependency in your domain layer.How can I help you explore Laravel packages today?