zaenalrfn / laravel-paypal-checkout
A secure and production-ready PayPal Checkout integration for Laravel with DTO-based payloads and verified webhooks
Package info
github.com/zaenalrfn/laravel-paypal-checkout
pkg:composer/zaenalrfn/laravel-paypal-checkout
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.8
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
README
A modern, clean, and well-tested PayPal payment gateway integration for Laravel 11 & 12.
Features
✅ Simple & Intuitive API - Fluent interface for creating orders and processing payments
✅ Webhook Support - Secure webhook verification and event handling
✅ Type-Safe DTOs - Immutable data transfer objects for better code quality
✅ Comprehensive Logging - Dedicated PayPal log channel for debugging
✅ Test Coverage - Well-tested codebase with Pest PHP
✅ Laravel 12 Ready - Compatible with Laravel 11 and 12
Installation
Install via Composer:
composer require zaenalrfn/laravel-paypal-checkout
Configuration
1. Publish Configuration
php artisan vendor:publish --tag=paypal-config
This creates config/paypal.php with all available options.
2. Environment Variables
Add your PayPal credentials to .env:
# PayPal Mode (sandbox or live) PAYPAL_MODE=sandbox # Sandbox Credentials PAYPAL_SANDBOX_CLIENT_ID=your-sandbox-client-id PAYPAL_SANDBOX_CLIENT_SECRET=your-sandbox-client-secret # Production Credentials (when ready) PAYPAL_LIVE_CLIENT_ID=your-live-client-id PAYPAL_LIVE_CLIENT_SECRET=your-live-client-secret # Webhook ID (get from PayPal Dashboard) PAYPAL_WEBHOOK_ID=your-webhook-id # Optional: Disable webhook verification in development PAYPAL_VERIFY_WEBHOOK=true
3. Get PayPal Credentials
- Go to PayPal Developer Dashboard
- Create an app or select existing one
- Copy Client ID and Secret
- For webhooks: Go to Webhooks → Create webhook → Copy Webhook ID
Usage
Creating a PayPal Order
use Zaenalrfn\LaravelPayPal\Facades\PayPal; use Zaenalrfn\LaravelPayPal\DTO\{CheckoutOrderData, PurchaseUnitData, AmountData}; // Build order data $order = CheckoutOrderData::capture() ->addPurchaseUnit( new PurchaseUnitData( new AmountData('USD', '10.00'), 'ORDER-123' // Optional reference ID ) ) ->withReturnUrl('https://yoursite.com/payment/success') ->withCancelUrl('https://yoursite.com/payment/cancel'); // Create order with PayPal $response = PayPal::checkout()->create($order); // Get approval URL $approvalUrl = collect($response['links']) ->firstWhere('rel', 'approve')['href']; // Redirect user to PayPal return redirect($approvalUrl);
Capturing a Payment
After user approves payment on PayPal:
use Zaenalrfn\LaravelPayPal\Facades\PayPal; // Get order ID from PayPal callback $orderId = $request->query('token'); // Capture the payment $response = PayPal::checkout()->capture($orderId); if ($response['status'] === 'COMPLETED') { // Payment successful! $captureId = $response['purchase_units'][0]['payments']['captures'][0]['id']; // Store in database, fulfill order, etc. }
Processing Refunds
use Zaenalrfn\LaravelPayPal\Facades\PayPal; $response = PayPal::payment()->refund($captureId, [ 'amount' => [ 'currency_code' => 'USD', 'value' => '10.00' ], 'note_to_payer' => 'Refund for order #123' ]);
Webhook Setup
1. Create Webhook in PayPal Dashboard
- Go to PayPal Developer Dashboard
- Select your app
- Click Webhooks → Add Webhook
- Webhook URL:
https://yoursite.com/api/paypal/webhook - Select events:
PAYMENT.CAPTURE.COMPLETEDPAYMENT.CAPTURE.DENIEDPAYMENT.CAPTURE.REFUNDED
- Save and copy the Webhook ID
- Add to
.env:PAYPAL_WEBHOOK_ID=your-webhook-id
2. Webhook Route
The package automatically registers the webhook route:
POST /api/paypal/webhook
3. Handle Webhook Events
Extend the WebhookHandler to customize event handling:
namespace App\PayPal; use Zaenalrfn\LaravelPayPal\Webhooks\WebhookHandler as BaseHandler; use Zaenalrfn\LaravelPayPal\Support\PayPalLogger; class CustomWebhookHandler extends BaseHandler { protected function paymentCompleted(array $event): void { $captureId = $event['resource']['id'] ?? null; // Update your database \App\Models\Payment::where('capture_id', $captureId) ->update(['status' => 'completed']); PayPalLogger::info('Payment completed', ['capture_id' => $captureId]); } }
Then bind it in your AppServiceProvider:
$this->app->bind( \Zaenalrfn\LaravelPayPal\Webhooks\WebhookHandler::class, \App\PayPal\CustomWebhookHandler::class );
Testing
Running Tests
cd packages/laravel-paypal
./vendor/bin/pest
Development Mode
For local testing without valid webhook signatures:
PAYPAL_VERIFY_WEBHOOK=false
⚠️ WARNING: Never disable webhook verification in production!
Logging
All PayPal interactions are logged to a dedicated channel:
storage/logs/paypal-{date}.log
View logs:
tail -f storage/logs/paypal-$(date +%Y-%m-%d).log
Troubleshooting
Webhook 500 Error
Problem: Webhook returns 500 Internal Server Error
Solutions:
- Wrong Webhook ID - Verify
PAYPAL_WEBHOOK_IDmatches PayPal Dashboard - Environment Mismatch - Sandbox webhook ID only works with sandbox credentials
- Development Testing - Set
PAYPAL_VERIFY_WEBHOOK=falsefor local testing
Authentication Failed
Problem: "PayPal authentication failed" error
Solutions:
- Verify
PAYPAL_SANDBOX_CLIENT_IDandPAYPAL_SANDBOX_CLIENT_SECRETare correct - Check
PAYPAL_MODEmatches your credentials (sandbox vs live) - Ensure credentials are from the correct PayPal app
Order Creation Failed
Problem: Order creation returns 400 error
Solutions:
- Verify amount format: must be string with 2 decimals (e.g., "10.00")
- Check currency code is valid (USD, EUR, GBP, etc.)
- Ensure at least one purchase unit is added
Security
- ✅ Webhook signature verification enabled by default
- ✅ No credentials in code (environment variables only)
- ✅ Dedicated exception handling with context
- ✅ Comprehensive logging for audit trails
License
The MIT License (MIT). Please see License File for more information.
Credits
Support
- Issues: GitHub Issues
- Documentation: PayPal API Docs