Zum Inhalt springen
← Zurück zu den Projekten

Laravel Paddle

Laravel Paddle

#Laravel Paddle

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads License

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-Signature verification (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 InvalidArgumentException before 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.

Neue Version verfügbar.