Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Paddle Laravel Package

protonemedia/laravel-paddle

Laravel Paddle offers a clean integration for Paddle billing in Laravel apps, including subscription management, checkout, webhook handling, and API helpers. It simplifies recurring payments, receipts, and customer data so you can add Paddle-powered billing quickly.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package and publish its config:

    composer require protonemedia/laravel-paddle
    php artisan vendor:publish --tag=paddle-config
    

    As of 2.10.0 the package supports Laravel 12 (the 2.x line now covers L10–L12), so it's safe to pull into a current app. Anything older than L9 will need to stay on a preceding release.

  2. Fill in the Paddle credentials in .env (Paddle Classic vendor credentials, not Paddle Billing API keys):

    PADDLE_VENDOR_ID=12345
    PADDLE_VENDOR_AUTH_CODE=...
    PADDLE_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----...
    PADDLE_SANDBOX=true
    
  3. Point Paddle's webhook settings at https://your-app.test/paddle/webhook (the package registers this route for you). In local dev, expose it with ngrok/expose and register the public URL in the Paddle dashboard.

  4. First use case — react to a new subscription instead of polling the API. Register a listener for the package's event:

    // app/Providers/EventServiceProvider.php (or bootstrap/providers.php on L11/L12)
    protected $listen = [
        \ProtoneMedia\LaravelPaddle\Events\SubscriptionCreated::class => [
            \App\Listeners\MarkUserSubscribed::class,
        ],
    ];
    

    Then in the listener, dump the event once (dd($event)) to see the exact payload/property names for your installed version, and persist the subscription_id against your user. That single flow — checkout → webhook → event → listener — is 90% of day-to-day usage.

Implementation Patterns

Server-side subscription creation. For flows you control (admin upgrades, custom signup), call the API directly through the facade:

use ProtoneMedia\LaravelPaddle\Facades\Paddle;

$response = Paddle::subscription()->create([
    'plan_id'        => 12345,
    'customer_email' => $user->email,
    'passthrough'    => json_encode(['user_id' => $user->id]),
]);

Client-side checkout with passthrough. For guest/one-off checkouts using Paddle.js overlay, always set passthrough to something that identifies your user (a user id, or better a short-lived signed token). The webhook payload is the only place that value comes back, and it is the canonical way to link a Paddle subscription to a local user.

One listener per business action. Keep listeners small and idempotent:

public function handle(SubscriptionCreated $event)
{
    $user = User::find(data_get(json_decode($event->payload['passthrough'] ?? '{}', true), 'user_id'));

    $user?->update([
        'paddle_subscription_id' => $event->payload['subscription_id'],
        'subscription_status'    => 'active',
    ]);
}

Plan changes and cancellations. Map SubscriptionUpdated (plan/quantity/proration changes) and SubscriptionCancelled to the same local subscription record. Use Paddle::subscription()->update($id, [...]) and Paddle::subscription()->cancel($id) when you initiate changes, but still let the webhook be the source of truth for state.

Custom alerts. The config ships an events map from Paddle alert_name → event class. Add your own entry and class if you need to handle an alert the package doesn't cover, rather than adding another route.

Local dev workflow. Flip PADDLE_SANDBOX=true, use sandbox keys/public key, and keep the tunnel URL registered. Sandbox and production have entirely separate vendor IDs, auth codes, and public keys — never mix them between environments.

Testing strategy. There is no Paddle::fake() shipped with the package, so put all Paddle SDK calls behind a thin application-level service you can bind/swallow in tests. For webhooks, either (a) unit-test listeners by constructing events with a fixture payload, or (b) feature-test the route by generating a valid p_signature with the matching private key in the test suite. Testing the route end-to-end is worth it — signature verification bugs are the #1 reason webhooks silently fail in staging.

Gotchas and Tips

  • This is Paddle Classic. The vendor ID/auth code model is the old Paddle API. If you're on Paddle Billing (the newer paddle-php-sdk v3, API keys, transaction.completed-style events), this package is not the right integration — don't try to force it. The 2.10.0 Laravel 12 bump doesn't change this; a framework-compat release is not a signal that the package is migrating to Billing.
  • Signature verification fails silently from Paddle's side. If the p_signature doesn't validate against PADDLE_PUBLIC_KEY, the middleware rejects the request (typically a 403) and Paddle retries. The most common causes are: sandbox vs. production key mismatch, a trailing newline trimmed in the env var, or an .env change without php artisan config:clear.
  • CSRF if you customize the route. The package's auto-registered route handles this for you. If you re-register it inside the web middleware group yourself, exclude it from VerifyCsrfToken or you'll get 419s.
  • Unknown alerts are dropped. If an alert isn't in the events config map, nothing is dispatched. When "nothing happens," first confirm the alert name is mapped, then add a catch-all log listener during debugging.
  • Idempotency is your job. Paddle retries webhooks. Guard state transitions (active → active is a no-op), and store an alert/subscription identifier so a replay doesn't double-grant access or double-credit a user.
  • Listeners run synchronously on the webhook request. Anything slow (email, provisioning, external API) belongs in a queued job dispatched from the listener, or you'll hit Paddle's webhook timeout.
  • passthrough is your only user link. If you don't set it at checkout/subscription creation, you cannot reliably associate the incoming webhook with a user — and you cannot retroactively recover it. Set it every time.
  • Deduplicate via payload, not local logic alone. Paddle Classic doesn't guarantee a unique event id in every alert; persist the raw payload (or a hash of it) alongside processed subscriptions so debugging and dedup are easy.
  • No built-in fakes. Any test that calls Paddle::...() will hit the network unless you wrap it. Introduce your own PaddleClient interface from day one.
  • Sparse release cadence — mostly compat bumps. Recent releases have been narrow (2.10.0 is just the Laravel 12 compatibility PR). Pin a version and read the changelog before upgrading; if a bump only touches framework constraints, a composer update plus a webhook smoke test is usually enough, but budget time for verification on anything that touches the event or config classes.
  • Log the raw webhook body in dev. Add a temporary middleware or log line at the start of a debug listener; it's far faster than reasoning about why a mapped event didn't fire.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource