PHPackages                             leonardganyire/paypal - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. leonardganyire/paypal

ActiveLibrary[Payment Processing](/categories/payments)

leonardganyire/paypal
=====================

Paypal integration for Laravel

1.0.0(today)01↑2900%MITPHPPHP ^8.3

Since Jun 19Pushed todayCompare

[ Source](https://github.com/ganyire/laravel-paypal)[ Packagist](https://packagist.org/packages/leonardganyire/paypal)[ RSS](/packages/leonardganyire-paypal/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (6)Versions (2)Used By (0)

PayPal for Laravel
==================

[](#paypal-for-laravel)

A frontend-agnostic Laravel package for PayPal Checkout Orders v2. It handles OAuth authentication, order creation, capture, refunds, and webhook signature verification. Your host app owns models, controllers, routes, and any JavaScript UI (Smart Buttons, redirect checkout, mobile API, etc.).

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11, 12, or 13

Installation
------------

[](#installation)

```
composer require leonardganyire/paypal
```

Publish the config file (optional — the package works with defaults merged automatically):

```
php artisan vendor:publish --tag=paypal-config
```

Configuration
-------------

[](#configuration)

Add these variables to your `.env`:

```
PAYPAL_MODE=sandbox
PAYPAL_CLIENT_ID=your-client-id
PAYPAL_CLIENT_SECRET=your-client-secret
PAYPAL_WEBHOOK_ID=your-webhook-id

# Optional overrides
# PAYPAL_BASE_URL=https://api-m.sandbox.paypal.com
# PAYPAL_TIMEOUT=12
# PAYPAL_CONNECT_TIMEOUT=4
# PAYPAL_ACCESS_TOKEN_CACHE_KEY=paypal.access_token
# PAYPAL_ACCESS_TOKEN_TTL=50
# PAYPAL_ACCESS_TOKEN_LEEWAY=60
```

VariableDescription`PAYPAL_MODE``sandbox` or `live`. Used to resolve the API base URL when `PAYPAL_BASE_URL` is not set.`PAYPAL_CLIENT_ID`PayPal REST app client ID.`PAYPAL_CLIENT_SECRET`PayPal REST app client secret.`PAYPAL_WEBHOOK_ID`Webhook ID from the PayPal Developer Dashboard. Required for webhook verification.`PAYPAL_BASE_URL`Explicit API base URL. If empty, resolved from `PAYPAL_MODE` (`https://api-m.sandbox.paypal.com` or `https://api-m.paypal.com`).Access tokens are cached automatically and cleared on 401 responses. The cache lifetime follows PayPal's reported `expires_in` value (typically several hours) minus `PAYPAL_ACCESS_TOKEN_LEEWAY` seconds, so the token is refreshed just before it expires and authentication round-trips are kept to a minimum. `PAYPAL_ACCESS_TOKEN_TTL` (in minutes) is used only as a fallback when PayPal omits `expires_in`.

Usage
-----

[](#usage)

The package registers a `PayPalClient` singleton. Resolve it via the facade or dependency injection.

### Facade

[](#facade)

```
use LeonardGanyire\Paypal\Facades\PayPal;

$order = PayPal::createOrder($payload, idempotencyKey: $paymentId);
$approvalUrl = PayPal::approvalUrl($order);
$capture = PayPal::captureOrder($order['id']);
```

### Dependency injection

[](#dependency-injection)

```
use LeonardGanyire\Paypal\PayPalClient;

final class CheckoutController
{
    public function __construct(
        private readonly PayPalClient $paypal,
    ) {}

    public function store(Request $request): RedirectResponse
    {
        $order = $this->paypal->createOrder([/* ... */]);

        return redirect()->away($this->paypal->approvalUrl($order));
    }
}
```

Checkout flow
-------------

[](#checkout-flow)

This package covers the PayPal API layer only. A typical one-time payment in your host app looks like this:

```
1. Create a local payment/order record
2. Call PayPal::createOrder() with purchase details
3. Send the buyer to PayPal (redirect) or return the order ID to your frontend (Smart Buttons)
4. After approval, call PayPal::captureOrder()
5. Mark your local payment as paid and fulfill the order
6. Optionally handle PayPal webhooks as a backup completion path

```

### Step 1 — Create a PayPal order

[](#step-1--create-a-paypal-order)

```
use LeonardGanyire\Paypal\Facades\PayPal;

$paypalOrder = PayPal::createOrder([
    'intent' => 'CAPTURE',
    'purchase_units' => [[
        'reference_id' => (string) $order->id,
        'description' => "Order {$order->id}",
        'amount' => [
            'currency_code' => strtoupper($order->currency),
            'value' => number_format($order->amount, 2, '.', ''),
        ],
    ]],
    'application_context' => [
        'return_url' => route('payments.paypal.return', $payment),
        'cancel_url' => route('payments.paypal.cancel', $payment),
        'brand_name' => config('app.name'),
        'user_action' => 'PAY_NOW',
    ],
], idempotencyKey: (string) $payment->id);

$paypalOrderId = $paypalOrder['id'];
$approvalUrl = PayPal::approvalUrl($paypalOrder);

// Store $paypalOrderId on your local payment record as provider_reference
```

Pass an idempotency key (e.g. your local payment ID) to avoid duplicate orders on retries.

### Step 2a — Redirect checkout

[](#step-2a--redirect-checkout)

Redirect the buyer to the approval URL:

```
return redirect()->away($approvalUrl);
```

After the buyer approves, PayPal redirects to your `return_url` with a `token` query parameter (the PayPal order ID). Validate it matches your stored `provider_reference`, then capture.

### Step 2b — JavaScript Smart Buttons (optional)

[](#step-2b--javascript-smart-buttons-optional)

If you use `@paypal/react-paypal-js` or the PayPal JS SDK in your frontend:

1. Your frontend calls your checkout endpoint and receives `{ paypalOrderId, paymentId }`.
2. The JS SDK handles approval in the popup.
3. Your frontend POSTs to a capture endpoint in your app.
4. Your capture endpoint calls `PayPal::captureOrder()`.

The package does not ship any frontend code — wire this up however your stack requires.

### Step 3 — Capture the payment

[](#step-3--capture-the-payment)

```
use LeonardGanyire\Paypal\Enums\PayPalOrderStatus;
use LeonardGanyire\Paypal\Exceptions\PayPalException;
use LeonardGanyire\Paypal\Facades\PayPal;

try {
    $capture = PayPal::captureOrder($paypalOrderId);

    $status = PayPalOrderStatus::fromResponse($capture);
    $captureId = PayPalOrderStatus::captureReference($capture);

    if ($status->isCompleted()) {
        // Mark local payment as paid, store $captureId, fulfill order
    }
} catch (PayPalException $exception) {
    // Mark local payment as failed
    // $exception->details() contains PayPal error info
}
```

### Step 4 — Return URL controller (example)

[](#step-4--return-url-controller-example)

```
public function __invoke(Request $request, Payment $payment): RedirectResponse
{
    $token = $request->query('token');

    if ($token !== $payment->provider_reference) {
        abort(403);
    }

    $capture = PayPal::captureOrder($token);
    $status = PayPalOrderStatus::fromResponse($capture);

    if ($status->isCompleted()) {
        // Complete local payment and redirect to success page
    }

    return redirect()->route('orders.show', $payment->order);
}
```

Other API methods
-----------------

[](#other-api-methods)

### Get an order

[](#get-an-order)

```
$order = PayPal::getOrder($paypalOrderId);
```

### Authorize (capture later)

[](#authorize-capture-later)

```
$authorization = PayPal::authorizeOrder($paypalOrderId);
```

### Refund a capture

[](#refund-a-capture)

```
// Full refund
$refund = PayPal::refundCapture($captureId);

// Partial refund
$refund = PayPal::refundCapture($captureId, [
    'amount' => [
        'value' => '5.00',
        'currency_code' => 'USD',
    ],
]);
```

Webhooks
--------

[](#webhooks)

Register a webhook URL in the PayPal Developer Dashboard pointing to a route in your app, then verify incoming events:

```
use Illuminate\Http\Request;
use LeonardGanyire\Paypal\Facades\PayPal;

public function __invoke(Request $request): Response
{
    $headers = [
        'paypal-auth-algo'         => $request->header('PAYPAL-AUTH-ALGO'),
        'paypal-cert-url'          => $request->header('PAYPAL-CERT-URL'),
        'paypal-transmission-id'   => $request->header('PAYPAL-TRANSMISSION-ID'),
        'paypal-transmission-sig'  => $request->header('PAYPAL-TRANSMISSION-SIG'),
        'paypal-transmission-time' => $request->header('PAYPAL-TRANSMISSION-TIME'),
    ];

    $payload = $request->all();

    if (! PayPal::verifyWebhookSignature($headers, $payload)) {
        abort(400);
    }

    $eventType = $payload['event_type'] ?? null;

    match ($eventType) {
        'CHECKOUT.ORDER.APPROVED',
        'PAYMENT.CAPTURE.COMPLETED' => $this->handlePaymentCompleted($payload),
        default => null,
    };

    return response('OK');
}
```

Exclude your webhook route from CSRF verification in `bootstrap/app.php`:

```
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'payments/paypal/webhook',
    ]);
})
```

Webhooks are a backup path. Your primary completion flow should still be the return URL or capture endpoint.

Status mapping
--------------

[](#status-mapping)

Use `PayPalOrderStatus` to interpret PayPal responses without coupling to your own payment enums:

```
use LeonardGanyire\Paypal\Enums\PayPalOrderStatus;

$status = PayPalOrderStatus::fromResponse($capture);

$status->isCompleted();  // true when status is COMPLETED
$status->isCancelled();  // true when status is VOIDED

$captureId = PayPalOrderStatus::captureReference($capture);
```

Supported statuses: `CREATED`, `SAVED`, `APPROVED`, `PAYER_ACTION_REQUIRED`, `VOIDED`, `COMPLETED`.

Error handling
--------------

[](#error-handling)

All API failures throw `LeonardGanyire\Paypal\Exceptions\PayPalException`:

```
use LeonardGanyire\Paypal\Exceptions\PayPalException;

try {
    PayPal::createOrder($payload);
} catch (PayPalException $exception) {
    $exception->getMessage();  // Human-readable message
    $exception->status();      // HTTP status (default 502)
    $exception->errorCode();   // paypal_request_failed
    $exception->details();     // ['status' => 400, 'name' => '...', 'debug_id' => '...']
}
```

Configuration errors (missing credentials, invalid base URL) also throw `PayPalException` before any HTTP request is made.

What this package does not include
----------------------------------

[](#what-this-package-does-not-include)

The following are intentionally left to your host app:

- Payment, order, or transaction models and migrations
- Checkout, return, capture, cancel, or webhook controllers and routes
- Order fulfillment logic (enrollment, subscription activation, etc.)
- Frontend JavaScript or React components
- PayPal Billing Subscriptions / recurring billing API

This keeps the package usable with any frontend (Inertia, Livewire, Blade, SPA, mobile API) and any payment architecture.

Testing
-------

[](#testing)

Run the package test suite:

```
composer test
```

In your host app, fake PayPal HTTP calls with Laravel's `Http::fake()`:

```
use Illuminate\Support\Facades\Http;

Http::fake([
    'api-m.sandbox.paypal.com/v1/oauth2/token' => Http::response([
        'access_token' => 'test-token',
        'expires_in' => 3600,
    ]),
    'api-m.sandbox.paypal.com/v2/checkout/orders' => Http::response([
        'id' => 'PAYPAL-ORDER-1',
        'links' => [['rel' => 'approve', 'href' => 'https://paypal.test/approve']],
    ], 201),
    'api-m.sandbox.paypal.com/v2/checkout/orders/PAYPAL-ORDER-1/capture' => Http::response([
        'status' => 'COMPLETED',
        'purchase_units' => [[
            'payments' => ['captures' => [['id' => 'CAPTURE-1']]],
        ]],
    ], 201),
]);
```

License
-------

[](#license)

MIT

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/ca9c1a5ec025668e5cc78632e6437c701421bf63027fffb9fbf772356d8918ae?d=identicon)[ganyirel](/maintainers/ganyirel)

---

Top Contributors

[![ganyire](https://avatars.githubusercontent.com/u/38717515?v=4)](https://github.com/ganyire "ganyire (5 commits)")

---

Tags

phplaravelpaymentspaypalintegration

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/leonardganyire-paypal/health.svg)

```
[![Health](https://phpackages.com/badges/leonardganyire-paypal/health.svg)](https://phpackages.com/packages/leonardganyire-paypal)
```

###  Alternatives

[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.5k](/packages/larastan-larastan)[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[spatie/laravel-responsecache

Speed up a Laravel application by caching the entire response

2.8k8.7M64](/packages/spatie-laravel-responsecache)[calebdw/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

15104.9k4](/packages/calebdw-larastan)[api-platform/laravel

API Platform support for Laravel

59156.3k11](/packages/api-platform-laravel)[ntanduy/cloudflare-d1-database

Cloudflare D1 database driver for Laravel — full Eloquent &amp; Query Builder support.

276.8k](/packages/ntanduy-cloudflare-d1-database)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
