PHPackages                             quellabs/canvas-payments-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. quellabs/canvas-payments-paypal

ActiveLibrary[Payment Processing](/categories/payments)

quellabs/canvas-payments-paypal
===============================

PayPal Orders v2 REST API payment gateway integration for the Canvas PHP framework

1.0.6(1mo ago)00MITPHPPHP ^8.2

Since Mar 19Pushed 2w agoCompare

[ Source](https://github.com/quellabs/canvas-payments-paypal)[ Packagist](https://packagist.org/packages/quellabs/canvas-payments-paypal)[ RSS](/packages/quellabs-canvas-payments-paypal/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (12)Versions (8)Used By (0)

PayPal Payment Provider
=======================

[](#paypal-payment-provider)

A PayPal payment provider for the Canvas framework. Part of the Canvas payments ecosystem. Implements the modern PayPal Orders v2 REST API with webhook-based payment notifications.

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

[](#installation)

```
composer require quellabs/canvas-payments-paypal
```

Architecture
------------

[](#architecture)

This package sits between the PayPal REST API and your application. Your application only ever touches the contracts layer — it never depends on this package directly. `PaymentRouter` (from `quellabs/canvas-payments`) discovers this package automatically via composer metadata and routes payment calls to it.

```
Your Application
      │
      ▼
PaymentRouter               (quellabs/canvas-payments — discovery + routing)
      │
      ▼
PaymentInterface            (quellabs/canvas-payments-contracts)
      │
      ▼
PayPal                      (this package — implements the interface)
      │
      ▼
PaypalGateway               (raw PayPal Orders v2 / Payments v2 REST API calls)

```

Webhook processing is decoupled from your application via signals. When PayPal sends a webhook notification, the package emits a `payment_exchange` signal carrying a `PaymentState`. Your application listens for that signal and handles it. The buyer return URL works the same way — both the return and cancel cases are handled by the same route and emit the same signal, so your application does not need to distinguish between them at the routing level.

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

[](#configuration)

Create `config/paypal.php` in your Canvas application:

```
return [
    'test_mode'         => true,
    'client_id'         => '',
    'client_secret'     => '',
    'webhook_id'        => '',
    'verify_ssl'        => true,
    'account_optional'  => true,
    'brand_name'        => '',
    'return_url'        => 'https://example.com/order/thankyou',
    'cancel_return_url' => 'https://example.com/order/cancelled',
    'webhook_url'       => 'https://example.com/webhooks/paypal',
];
```

KeyRequiredDescription`test_mode`YesSet to `true` for sandbox, `false` for production`client_id`YesREST API client ID from the PayPal Developer Dashboard`client_secret`YesREST API client secret from the PayPal Developer Dashboard`webhook_id`YesWebhook ID from your PayPal app settings — required for signature verification`verify_ssl`NoWhether to verify PayPal's SSL certificate. Always `true` in production. Defaults to `true``account_optional`NoWhen `true`, buyers can check out as a guest without a PayPal account. Defaults to `true``brand_name`NoYour store name shown on the PayPal checkout page. Leave empty to use your PayPal account name`return_url`YesURL the customer is redirected to after a completed payment`cancel_return_url`YesURL the customer is redirected to after cancelling at PayPal`webhook_url`YesFull URL PayPal POSTs webhook notifications to — must match the URL registered in your PayPal appUsage
-----

[](#usage)

### Initiating a payment

[](#initiating-a-payment)

Inject `PaymentInterface` via Canvas DI and call `initiate()`:

```
use Quellabs\Payments\Contracts\PaymentInterface;
use Quellabs\Canvas\Controllers\BaseController;
use Quellabs\Payments\Contracts\PaymentRequest;
use Quellabs\Payments\Contracts\PaymentInitiationException;

class CheckoutController extends BaseController {

    public function __construct(private PaymentInterface $router) {}

    /**
     * @Route("...")
     */
    public function checkout(): void {
        $request = new PaymentRequest(
            paymentModule: 'paypal',
            amount:        999,   // in minor units — €9.99
            currency:      'EUR',
            description:   'Order #12345',
        );

        try {
            $result = $this->router->initiate($request);
            return $this->redirect($result->redirectUrl);
        } catch (PaymentInitiationException $e) {
            // handle error
        }
    }
}
```

### Handling refunds

[](#handling-refunds)

Pass `amount: null` for a full refund, or a minor-unit integer for a partial refund.

When your `payment_exchange` listener receives a `PaymentStatus::Paid` state, store `$state->metadata['paymentReference']` — you'll need it as `RefundRequest::$paymentReference`.

```
// In your payment_exchange listener — store the capture ID when the payment succeeds
public function onPaymentExchange(PaymentState $state): void {
    if ($state->state === PaymentStatus::Paid) {
        $this->orderRepository->updateCaptureId(
            $state->transactionId,
            $state->metadata['paymentReference']
        );
    }
}

// Full refund
$request = new RefundRequest(
    paymentReference: $order->paymentReference, // retrieved from your orders table
    paymentModule:   'paypal',
    amount:           null, // null = full refund
    currency:         'EUR',
    description:      'Full refund for order #12345',
);

// Partial refund
$request = new RefundRequest(
    paymentReference: $order->paymentReference, // retrieved from your orders table
    paymentModule: 'paypal',
    amount:        500,                 // in minor units — €5.00
    currency:      'EUR',
    description:   'Partial refund for order #12345',
);

try {
    $result = $this->router->refund($request);
    echo $result->refundId;
} catch (PaymentRefundException $e) {
    // handle error
}
```

### Listening for payment state changes

[](#listening-for-payment-state-changes)

```
use Quellabs\Canvas\Annotations\ListenTo;
use Quellabs\Payments\Contracts\PaymentState;
use Quellabs\Payments\Contracts\PaymentStatus;

class OrderService {

    /**
     * @ListenTo("payment_exchange")
     */
    public function onPaymentExchange(PaymentState $state): void {
        match ($state->state) {
            PaymentStatus::Paid      => $this->markPaid($state->transactionId),
            PaymentStatus::Canceled  => $this->markCanceled($state->transactionId),
            PaymentStatus::Failed    => $this->markFailed($state->transactionId),
            default                  => null,
        };
    }
}
```

PayPal-specific quirks
----------------------

[](#paypal-specific-quirks)

### Two transaction identifiers

[](#two-transaction-identifiers)

PayPal uses two different identifiers across the payment lifecycle:

- **Order ID** — created by `POST /v2/checkout/orders` and returned by `initiate()` as `InitiateResult::$transactionId`. Used to drive the checkout flow and passed to `exchange()`.
- **Capture ID** — available in `PaymentState::$metadata['paymentReference']` when a `PaymentStatus::Paid` event fires. **Persist this value** — it is required as `RefundRequest::$paymentReference` for refunds and `getRefunds()`.

### Webhooks vs. return URL

[](#webhooks-vs-return-url)

PayPal notifies your application of payment state changes in two independent ways:

- **Webhooks** — a server-to-server POST from PayPal to `webhook_url`, verified by cryptographic signature. This is the authoritative source of truth and may arrive before or after the buyer returns to your site. Only `PAYMENT.CAPTURE.*` events trigger a signal; all other event types are acknowledged and ignored.
- **Return URL** — a browser redirect after the buyer completes or cancels at PayPal.

Both routes call `exchange()` and emit the `payment_exchange` signal. Your application should be idempotent when handling this signal, as it may fire twice for the same transaction.

### Webhook setup

[](#webhook-setup)

Register your `webhook_url` in the [PayPal Developer Dashboard](https://developer.paypal.com/dashboard/) under Apps &amp; Credentials → your app → Webhooks. Subscribe at minimum to these event types:

- `PAYMENT.CAPTURE.COMPLETED`
- `PAYMENT.CAPTURE.DENIED`
- `PAYMENT.CAPTURE.REFUNDED`
- `PAYMENT.CAPTURE.REVERSED`

Copy the resulting Webhook ID into your `config/paypal.php` as `webhook_id`. Without it, all webhook notifications will be rejected.

### Refund type determination

[](#refund-type-determination)

Pass `amount: null` to `RefundRequest` for a full refund. The driver omits the amount field from the API request, which causes PayPal to refund the full captured amount internally. Pass a minor-unit integer for a partial refund. Do not attempt to calculate the refundable amount yourself and pass it as a full refund — use `null` instead.

License
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 85% of packages

Maintenance95

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

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

Every ~11 days

Recently: every ~16 days

Total

7

Last Release

34d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/57e4ab872b3e37536367f2d26b192df3d3bb6a6a1cebec9a104d14a6d2ffe157?d=identicon)[noescom](/maintainers/noescom)

---

Tags

paymentspaypalcanvaspayment gatewaypaypal restcanvas-paymentspaypal-orders

### Embed Badge

![Health badge](/badges/quellabs-canvas-payments-paypal/health.svg)

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

PHPackages © 2026

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