jeffersongoncalves / laravel-paddle
PHP/Laravel client for the Paddle Billing API v1: products, prices, customers, subscriptions, transactions, discounts, adjustments, events, notifications and webhook signature verification.
Package info
github.com/jeffersongoncalves/laravel-paddle
pkg:composer/jeffersongoncalves/laravel-paddle
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.21
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel Paddle
A PHP/Laravel client for the Paddle Billing API v1. Covers products, prices, customers, subscriptions, transactions, discounts, adjustments, events and notifications through a simple, typed API built on Laravel's Http client — plus verification of Paddle's webhook signatures.
Features
- Products: list, get, create, update, archive
- Prices: list, get, create, update, archive
- Customers: list, get, create, update, archive, credit balances
- Subscriptions: list, get, update, cancel, pause, resume, activate, one-off charge, payment-method transaction
- Transactions: list, get, create, update, invoice URL
- Discounts: list, get, create, update, archive
- Adjustments: list, create (refund/credit/chargeback), credit-note URL
- Events: event stream and event-type catalog
- Notifications: list, get, replay, delivery logs
- Webhooks:
Paddle-Signatureverification (HMAC-SHA256, replay-window check) - Sandbox support via a single env flag
- Throws
PaddleException(with the original API error body and Paddle error code) on any non-2xx response - Throws
InvalidArgumentExceptionbefore hitting the API when a required field is missing
Installation
You can install the package via composer:
composer require jeffersongoncalves/laravel-paddle
Publish the config file:
php artisan vendor:publish --tag=paddle-config
Set your credentials in .env:
PADDLE_API_KEY=your-api-key PADDLE_SANDBOX=false PADDLE_WEBHOOK_SECRET=your-notification-secret
Create the API key under Developer Tools > Authentication in Paddle. Sandbox and live keys are separate — a sandbox key only works with PADDLE_SANDBOX=true.
Configuration
// config/paddle.php return [ 'api_key' => env('PADDLE_API_KEY'), 'sandbox' => (bool) env('PADDLE_SANDBOX', false), 'base_url' => env('PADDLE_BASE_URL', env('PADDLE_SANDBOX', false) ? 'https://sandbox-api.paddle.com' : 'https://api.paddle.com'), 'webhook_secret' => env('PADDLE_WEBHOOK_SECRET'), 'default_per_page' => env('PADDLE_DEFAULT_PER_PAGE', 50), ];
Usage
Use the Paddle facade or inject JeffersonGoncalves\Paddle\Paddle. Each API group is exposed as a method returning a dedicated resource class. Every list() accepts Paddle's own filters (status, after, per_page, order_by, id, ...) and defaults per_page to default_per_page.
Products and prices
use JeffersonGoncalves\Paddle\Facades\Paddle; $products = Paddle::products()->list(['status' => 'active']); $product = Paddle::products()->create([ 'name' => 'Pro Plan', 'tax_category' => 'saas', 'description' => 'Everything in one plan', ]); Paddle::products()->update($product['data']['id'], ['name' => 'Pro Plan (annual)']); Paddle::products()->archive($product['data']['id']); $price = Paddle::prices()->create( productId: $product['data']['id'], amount: 1990, // lowest denomination — 1990 = US$ 19.90 currencyCode: 'USD', description: 'Monthly', attributes: ['billing_cycle' => ['interval' => 'month', 'frequency' => 1]], );
Customers
$customer = Paddle::customers()->create('jane@example.com', ['name' => 'Jane Doe']); Paddle::customers()->update($customer['data']['id'], ['name' => 'Janet Doe']); Paddle::customers()->creditBalances($customer['data']['id']); Paddle::customers()->archive($customer['data']['id']);
Subscriptions
$subscriptions = Paddle::subscriptions()->list(['customer_id' => 'ctm_123']); Paddle::subscriptions()->update('sub_123', [ 'proration_billing_mode' => 'prorated_immediately', 'items' => [['price_id' => 'pri_123', 'quantity' => 3]], ]); Paddle::subscriptions()->cancel('sub_123'); // at period end Paddle::subscriptions()->cancel('sub_123', 'immediately'); Paddle::subscriptions()->pause('sub_123', '2026-12-01T00:00:00Z'); // omit to pause indefinitely Paddle::subscriptions()->resume('sub_123'); Paddle::subscriptions()->activate('sub_123'); // trialing -> active // One-off charge on top of the subscription Paddle::subscriptions()->charge('sub_123', [['price_id' => 'pri_123', 'quantity' => 1]], 'immediately'); // Transaction that lets the customer update their card Paddle::subscriptions()->updatePaymentMethodTransaction('sub_123');
Transactions, discounts and adjustments
$transaction = Paddle::transactions()->create( items: [['price_id' => 'pri_123', 'quantity' => 1]], attributes: ['customer_id' => 'ctm_123', 'collection_mode' => 'manual'], ); Paddle::transactions()->invoice($transaction['data']['id']); Paddle::discounts()->create(25, 'percentage', 'Launch', ['code' => 'LAUNCH25']); // Refund a transaction item Paddle::adjustments()->create( transactionId: $transaction['data']['id'], action: 'refund', reason: 'Customer request', items: [['item_id' => 'txnitm_123', 'type' => 'full']], );
Events and notifications
Paddle::events()->list(['after' => 'evt_123']); Paddle::events()->types(); Paddle::notifications()->list(['status' => 'failed']); Paddle::notifications()->logs('ntf_123'); Paddle::notifications()->replay('ntf_123');
Webhooks
Verify the Paddle-Signature header against the raw request body before trusting a webhook:
use Illuminate\Http\Request; use JeffersonGoncalves\Paddle\Facades\Paddle; Route::post('/paddle/webhook', function (Request $request) { abort_unless(Paddle::webhooks()->verifyRequest($request), 403); $event = $request->json()->all(); // handle $event['event_type'] ... return response()->noContent(); })->withoutMiddleware([VerifyCsrfToken::class]);
verify(string $payload, string $signature, int $maxAge = 5) is available when you already hold the raw body. Signatures older than $maxAge seconds are rejected (pass 0 to disable that check). The payload must be the raw body — a re-encoded array will not match.
Error handling
Any non-2xx API response throws JeffersonGoncalves\Paddle\Exceptions\PaddleException, which exposes both the decoded error body and Paddle's machine-readable code:
use JeffersonGoncalves\Paddle\Exceptions\PaddleException; try { Paddle::subscriptions()->get('sub_missing'); } catch (PaddleException $e) { if ($e->errorCode() === 'entity_not_found') { // ... } logger()->error($e->getMessage(), $e->errorBody()); }
Missing required fields (e.g. name/tax_category on products()->create()) throw InvalidArgumentException before any HTTP call is made.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.
