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.
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.
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
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.
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.
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.
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.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.web middleware group yourself, exclude it from VerifyCsrfToken or you'll get 419s.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.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.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.Paddle::...() will hit the network unless you wrap it. Introduce your own PaddleClient interface from day one.composer update plus a webhook smoke test is usually enough, but budget time for verification on anything that touches the event or config classes.How can I help you explore Laravel packages today?