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

jeffersongoncalves/laravel-paddle

Laravel Paddle provides a clean integration for Paddle billing in Laravel applications, simplifying subscriptions, payments, checkout flows, webhooks, and API configuration through familiar Laravel conventions.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and configure.

    composer require <vendor>/paddle-billing   # PHP 8.2+, Laravel 12/13
    php artisan vendor:publish --tag=paddle-config
    

    Set the env vars the package's config file reads — typically PADDLE_API_KEY, PADDLE_WEBHOOK_SECRET, PADDLE_SANDBOX, plus a client-side token if you use Paddle.js. Run php artisan config:cache afterwards.

  2. Look here first, in this order:

    • config/paddle.php — the only place that knows about sandbox vs. production; read it before writing code.
    • The service provider — shows whether the client is a singleton, how it's bound, and whether it's swappable (important for testing).
    • The resources directory — one class per API area (products, prices, customers, subscriptions, transactions, discounts, adjustments, events, notifications). This is your map of what exists.
    • PaddleException — your entire error-handling strategy hangs off its error body + machine-readable error code.
    • The webhook verification code — confirm exactly what it expects (raw body, header name, tolerance) before wiring the route.
  3. First use case: a server-side checkout. Resolve a price_id you control, create a transaction via the transactions resource with the customer and price, then hand the returned transaction.id to Paddle.js Checkout on the front end. Do not post prices from the browser. Getting this loop working end-to-end (create transaction → complete in sandbox → receive webhook) is the fastest way to validate your config, keys, and webhook secret in one pass.

Implementation Patterns

Wrap the client, don't sprinkle it. The package is a thin API wrapper. Put it behind your own actions/services (CreateSubscriptionCheckout, SyncSubscriptionFromWebhook) so that resource calls, DTO mapping to your Eloquent models, and error translation live in one layer. Controllers and jobs then call your actions, and you can swap or fake the transport later.

Mirror state locally, driven by webhooks. Keep customers, subscriptions, and transactions tables keyed by Paddle IDs (ctm_, sub_, txn_). Treat webhooks as the source of truth for state transitions and the API as a source of truth for reads you don't have locally. Don't poll subscriptions()->get() on page load to decide whether a user has access.

Webhook pipeline. Route it in api.php (or otherwise exclude CSRF), verify the signature before decoding, persist the raw payload to a webhook_events table with a unique index on the event ID, dispatch a queued job, and return 200 immediately. Idempotency on the event ID is non-negotiable — Paddle will retry.

Handle events by type, not by shape. subscription.created, subscription.updated, subscription.canceled, transaction.completed, adjustment.created (refunds/credit notes) each map to a small handler. Keep a default branch that logs and returns 200 so new event types don't blow up your endpoint.

Errors as a first-class path.

try {
    $transaction = $paddle->transactions()->create($payload);
} catch (PaddleException $e) {
    Log::warning('paddle.error', ['code' => $e->getCode(), 'body' => $e->getBody()]);
    // map known codes to user-facing messages, rethrow the rest
}

Branch on the machine-readable code, never on the message string. Retry only on 5xx/429 with backoff; a 4xx means your payload is wrong.

Testing. Because the package almost certainly rides on Laravel's HTTP client, Http::fake() around the resource calls is usually enough to assert the exact request body and to return canned Paddle responses. Bind a fake client in the container for unit tests; hit the sandbox only in a small, tagged integration suite.

Gotchas and Tips

  • Raw body for signatures. HMAC verification must run on $request->getContent(). Any middleware that re-encodes JSON, or code that verifies against $request->all(), will fail intermittently and confusingly. Verify before any parsing.
  • The replay window is a clock check. If you're behind a proxy or your server clock drifts, valid webhooks get rejected. Keep NTP running, and log the timestamp delta when verification fails so you can tell a clock problem from a wrong-secret problem.
  • Sandbox is a separate universe. Different API keys, different webhook destination and secret, entirely different product/price/customer IDs. Never let price IDs leak between environments — resolve them from config per environment and validate the incoming price ID against an allowlist.
  • Amounts are integers in the smallest currency unit. No floats, no local tax math. Paddle computes tax; your job is to display what it returns.
  • Use idempotency keys on POSTs. Retries after a timeout are the classic duplicate-charge bug; send a stable key derived from your own order/attempt ID.
  • Webhooks can arrive out of order. Compare occurred_at (or a monotonic local state machine) before overwriting a subscription status. A late subscription.updated must not resurrect a cancelled subscription.
  • Rate limits and 429s. Wrap write paths in a retry with jitter; queue bulk backfills rather than looping synchronously in a request.
  • request_id is your support ticket. Paddle's error body carries one — log it alongside the code and body, and include it in exception reports.
  • Extension points worth knowing: the container binding for the client (swap it for a fake), the config file (base URL override if you need a proxy/mock server), and the exception class (subclass it if you want typed exceptions per resource).
  • Version constraint is a real gate. Laravel 12/13 + PHP 8.2+ means apps on 10/11 can't install this at all — check before promising an upgrade path, and pin the package version in composer.json since a 1.0.x API surface can still shift.
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib