Accepting PayPal Payments in Laravel with drewdan/paypal
drewdan/paypal is an open source Laravel package for accepting PayPal payments. It uses a builder pattern to construct requests, strong types and enums throughout, and follows familiar Laravel conventions to keep the integration feeling native.
Note: This package currently supports up to Laravel 10. Support for Laravel 11 is in progress. It is not yet feature complete — contributions and PRs are very welcome.
Installation
Install the package via Composer:
composer require drewdan/paypal
Publish the config file:
php artisan vendor:publish --tag=drewdan-paypal-config
The config file is where you will register your webhook event handlers later on.
Add the following to your .env file, using the credentials from your PayPal developer account:
PAYPAL_CLIENT_ID=
PAYPAL_SECRET=
PAYPAL_MODE=LIVE
Set PAYPAL_MODE to SANDBOX to test against PayPal's sandbox environment without processing real transactions.
Creating an Order
The package uses builders to construct PayPal API requests. Here is a complete example of generating an order and redirecting the user to PayPal to complete payment:
$experienceContext = ExperienceContext::make()
->setBrandName(config('app.name'))
->setShippingPreference(ShippingPreferenceEnum::NO_SHIPPING)
->setUserAction(UserActionEnum::PAY_NOW)
->setReturnUrl(route('checkout.success', ['order' => $uuid, 'transaction' => $transactionUuid]))
->setCancelUrl(
route('checkout', [
'status' => 'cancelled',
'order' => $uuid,
'transaction' => $transactionUuid,
]),
);
$order = Order::builder()
->setIntent(PaymentIntentEnum::CAPTURE)
->setPaymentSource(
Paypal::make()
->setExperienceContext($experienceContext)
->setEmailAddress($user->email)
)
->addPurchaseUnit(
PurchaseUnit::make()
->setReferenceId($transactionUuid)
->setAmount(10.00, 'GBP')
)
->create();
$paypalRedirectLink = $order->getPaymentRedirectUrl();
This creates an order in PayPal and returns a redirect URL. Send the user there to complete payment.
Breaking It Down
Order::builder() returns an OrderBuilder instance used to construct and create the order. All methods are strongly typed with enums, so your IDE will guide you through the available options.
->setIntent(PaymentIntentEnum::CAPTURE)
The intent determines what happens when the customer approves the payment. Two options are available:
CAPTURE— funds are debited immediately on approval. Use this for standard purchases.AUTHORIZE— the payment is reserved but not taken. Useful when you need to perform checks before charging the customer. Deliveroo is a good example: they authorise the payment when you place an order, then capture it only once the restaurant accepts — if the restaurant declines, the authorisation is released and nothing is charged.
->setPaymentSource(...)
The payment source determines how the order is funded. Currently the package supports Paypal and Token as payment sources. The Paypal source loads the hosted PayPal checkout screen, which is the most common flow for online stores.
Most payment sources require an experience context, and some accept additional source-specific options — for example, passing setEmailAddress() pre-fills the customer's email on the PayPal form.
Experience context configures the journey the customer has on the hosted PayPal page:
$experienceContext = ExperienceContext::make()
->setBrandName(config('app.name'))
->setShippingPreference(ShippingPreferenceEnum::NO_SHIPPING)
->setUserAction(UserActionEnum::PAY_NOW)
->setReturnUrl(...)
->setCancelUrl(...);
setBrandName— displayed on the PayPal order page so customers know where they are.setShippingPreference— controls whether PayPal collects a shipping address. In most casesNO_SHIPPINGis correct as your own checkout handles this.setUserAction—PAY_NOWcharges the customer immediately on approval;CONTINUEreturns them to a point in your checkout for further steps before capture.setReturnUrl/setCancelUrl— where PayPal redirects the user on success or cancellation.
->addPurchaseUnit(...)
A purchase unit represents what the customer is buying and how much they will be charged. Currently supports a reference ID, amount, and currency. Item-level breakdown is planned for a future release.
Retrieving an Order
Much like Eloquent, the package uses models to represent PayPal resources. Retrieve an existing order by its ID:
use Drewdan\Paypal\Orders\Models\Order;
$order = Order::retrieve('some-order-id');
Available Methods
$order->authorize(); // Authorize the payment
$order->capture(); // Capture the payment
$order->getPurchaseUnits(); // Returns a Collection of purchase units
$order->listCaptures(); // Returns a Collection of captures
$order->toArray(); // Array representation, null values removed
The Order class is not final and its properties are public, so you can extend it and add your own methods.
Order Model Properties
| Type | Property | Description |
|---|---|---|
string | id | The PayPal order ID |
string | create_time | When the order was created |
string | update_time | When the order was last updated |
string | processing_instruction | Processing instruction string |
string | intent | The payment intent |
OrderStatusEnum | status | CREATED, SAVED, APPROVED, VOIDED, COMPLETED, PAYER_ACTION_REQUIRED |
PurchaseUnits | purchase_units | The purchase units on this order |
Amount | gross_amount | Amount and currency |
BuildsPaymentSource | payment_source | The payment source (most commonly Paypal) |
Links | links | All links associated with this resource |
array | payer | Payer data |
Webhooks
The package registers routes to handle incoming PayPal webhook events. Webhook handlers are configured in the published paypal.php config file.
⚠️ Important: Webhook handling is not yet safe for production use. The package does not currently validate the authenticity of incoming events. Use in development environments only. Automatic signature validation is planned for the next major release.
Registering Handlers
Open config/paypal.php and find the webhook.handlers array. Keys are WebhookEventEnum values; values are either a class implementing HandlesPaypalWebhookEvent or a closure.
'webhook' => [
'handlers' => [
WebhookEventEnum::CHECKOUT_ORDER_APPROVED->value => PaypalCheckoutOrderApproved::class,
],
],
Using a closure instead of a class will prevent config caching (
php artisan config:cache). Classes are recommended.
Register as many handlers as you need. Once configured, run the following Artisan command to create the webhooks in your PayPal account:
php artisan paypal:webhook-setup
Caution: This command deletes all existing webhooks on the account and recreates only those defined in your config. Take care if multiple applications share the same PayPal account.
Implementing a Handler
The HandlesPaypalWebhookEvent interface requires a handle method that receives a WebhookEvent. Call getResource() on the event to get the underlying model:
class PaypalCheckoutOrderApproved implements HandlesPaypalWebhookEvent
{
public function handle(WebhookEvent $event): void
{
$resource = $event->getResource();
$id = $resource->id;
$status = $resource->status;
// Look up your order, update its status, trigger fulfilment, etc.
}
}
Registering Webhooks Manually
If you need more control — for example, multiple webhook endpoints pointing to different URLs — you can use the builder directly instead of the Artisan command:
$webhook = Webhook::builder()
->setUrl('https://example.com/example_webhook')
->setEvents([
WebhookEventEnum::PAYMENT_AUTHORIZATION_CREATED,
WebhookEventEnum::PAYMENT_CAPTURE_COMPLETED,
])
->create();
setEvents accepts an array or Collection of WebhookEventEnum values.
Note: if you later run
paypal:webhook-setup, it will remove any webhooks registered this way.
Supported Webhook Events
enum WebhookEventEnum: string
{
// Payments V2
case PAYMENT_AUTHORIZATION_CREATED = 'PAYMENT.AUTHORIZATION.CREATED';
case PAYMENT_AUTHORIZATION_VOIDED = 'PAYMENT.AUTHORIZATION.VOIDED';
case PAYMENT_CAPTURE_DECLINED = 'PAYMENT.CAPTURE.DECLINED';
case PAYMENT_CAPTURE_COMPLETED = 'PAYMENT.CAPTURE.COMPLETED';
case PAYMENT_CAPTURE_PENDING = 'PAYMENT.CAPTURE.PENDING';
case PAYMENT_CAPTURE_REFUNDED = 'PAYMENT.CAPTURE.REFUNDED';
case PAYMENT_CAPTURE_REVERSED = 'PAYMENT.CAPTURE.REVERSED';
// Orders V2
case CHECKOUT_ORDER_COMPLETED = 'CHECKOUT.ORDER.COMPLETED';
case CHECKOUT_ORDER_APPROVED = 'CHECKOUT.ORDER.APPROVED';
case CHECKOUT_ORDER_SAVED = 'CHECKOUT.ORDER.SAVED';
case CHECKOUT_ORDER_VOIDED = 'CHECKOUT.ORDER.VOIDED';
case CHECKOUT_PAYMENT_APPROVAL_REVERSED = 'CHECKOUT.PAYMENT-APPROVAL.REVERSED';
// Disputes
case CUSTOMER_DISPUTE_CREATED = 'CUSTOMER.DISPUTE.CREATED';
case CUSTOMER_DISPUTE_RESOLVED = 'CUSTOMER.DISPUTE.RESOLVED';
case CUSTOMER_DISPUTE_UPDATED = 'CUSTOMER.DISPUTE.UPDATED';
/** @deprecated */
case RISK_DISPUTE_CREATED = 'RISK.DISPUTE.CREATED';
}
Contributing
The package is open source and not yet feature complete. Planned work includes Laravel 11 support, item-level purchase unit breakdown, additional payment sources (Venmo etc.), and webhook signature validation.
PRs and issues are welcome on GitHub.
Have thoughts on this? Get in touch.

