andriichuk / laravel-billing
Vendor-agnostic subscription billing primitives for Laravel.
Requires
- php: ^8.5
- ext-json: *
- illuminate/bus: ^13.0
- illuminate/config: ^13.0
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/http: ^13.0
- illuminate/queue: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-29 04:05:43 UTC
README
A vendor-agnostic subscription billing core for Laravel 13 and PHP 8.5. It provides a Cashier-like application API while keeping Stripe, Paddle, BlueSnap, and every other provider in separate driver packages.
Stability: This package is pre-1.0. Its contracts are intentionally being validated against the first external BlueSnap driver before a stable release. The current development release is available on Packagist.
Architecture
The payment provider is authoritative. Laravel Billing stores a normalized local projection for application queries, authorization, UI, reporting, webhook recovery, and reconciliation. The core knows stable billing concepts only and has no provider SDK dependency.
Drivers expose small capability contracts instead of one mandatory mega-interface. Every stored customer, subscription, transaction, and webhook includes its driver name, so one billable model can use several providers simultaneously. Provider responses retain sanitized raw data, command DTOs accept providerOptions, and Billing::driver() provides an intentional escape hatch to a driver.
See docs/architecture.md for the full design.
Requirements and installation
- PHP 8.5+
- Laravel 13
- A configured queue worker for production webhooks
Install the current development release from Packagist:
composer require andriichuk/laravel-billing:dev-main
Laravel package discovery registers the service provider. Publish configuration and migrations, then migrate:
php artisan vendor:publish --tag=billing-config php artisan vendor:publish --tag=billing-migrations php artisan migrate
The published configuration selects a default driver, driver-specific configuration, replaceable model classes, webhook queue routing, retention, and reconciliation chunk size.
Billable models
Add the trait to an Eloquent model:
use Andriichuk\LaravelBilling\Concerns\Billable; class User extends Authenticatable { use Billable; }
The local query API is driver-aware:
$user->billingCustomer(); $user->billingCustomer('stripe'); $user->subscriptions()->latest()->get(); $user->subscription('default', driver: 'bluesnap'); $user->subscribed('default', price: 'price-123', driver: 'stripe');
Installing and selecting drivers
Provider packages register themselves with Laravel's extension mechanism:
use Andriichuk\LaravelBilling\Billing; Billing::extend('acme', function ($app, array $config) { return new AcmeBillingDriver(/* provider dependencies */); });
Set BILLING_DRIVER=acme for the default, or choose explicitly:
$driver = Billing::driver(); $acme = Billing::driver('acme'); $same = $user->billing('acme');
Customers
Creating or synchronizing a customer calls the selected provider and updates the local projection:
$customer = $user->createBillingCustomer( driver: 'acme', name: $user->name, email: $user->email, metadata: ['account_tier' => 'pro'], providerOptions: ['locale' => 'en'], ); $customer = $user->syncBillingCustomer('acme', name: 'Updated Name');
Subscriptions
The builder validates common input, creates typed command data, and delegates remote work to ManagesSubscriptions:
$subscription = $user->newSubscription('default', 'price-123') ->driver('acme') ->quantity(5) ->trialDays(14) ->paymentMethod('pm-opaque-reference') ->withMetadata(['team' => 'platform']) ->withProviderOptions(['provider_specific_key' => 'value']) ->idempotencyKey('subscription:user:'.$user->getKey()) ->create();
Money uses validated decimal strings and uppercase ISO 4217 codes. Floats are intentionally not accepted. Cancellation drivers receive an explicit CancellationMode::AtPeriodEnd or CancellationMode::Immediately; pause, proration, quantity changes, and plan changes are separate optional capabilities.
Normalized subscription states are active, trialing, past_due, paused, canceled, finished, incomplete, and unknown. Local models expose active(), onTrial(), onGracePeriod(), recurring(), and valid() helpers without claiming that every provider has identical lifecycle behavior.
Capability checks
use Andriichuk\LaravelBilling\Enums\Capability; if (Billing::driver('acme')->supports(Capability::Refunds)) { // Resolve the driver and use its SupportsRefunds contract. }
Core orchestration throws UnsupportedCapability with the driver and capability names when an unavailable operation is attempted.
Webhooks and queues
The package registers:
POST /billing/webhooks/{driver}
The driver verifies the exact raw body before anything is persisted. Valid events are sanitized, stored idempotently in billing_webhook_events, and handed to a unique queue job. The job applies normalized events under database transactions and records attempts, status, errors, and timestamps. Unknown valid events remain available with ignored status and dispatch WebhookIgnored.
Configure a durable queue in production and run a worker:
QUEUE_CONNECTION=redis BILLING_DRIVER=acme
php artisan queue:work --queue=billing,default php artisan billing:webhooks:retry --driver=acme php artisan billing:webhooks:prune --dry-run php artisan billing:webhooks:prune --days=90 --force
See docs/webhooks.md before exposing a provider endpoint.
Reconciliation
Reconciliation refreshes the local projection from authoritative provider data:
php artisan billing:reconcile
php artisan billing:reconcile --driver=acme
php artisan billing:reconcile --model=subscription
php artisan billing:reconcile --model=subscription --id=123
php artisan billing:reconcile --driver=acme --dry-run
php artisan billing:reconcile --driver=acme --cursor=123 --page-size=250
php artisan billing:reconcile --driver=acme --since="2026-09-01T00:00:00Z"
php artisan billing:reconcile --model=subscription --id=123 --force
Drivers implement ReconcilesResources and stream normalized provider resources. A model without an ID requests a sweep; a model and ID request one resource. since is provider-dependent, while cursor and pageSize let scheduled jobs resume bounded pages.
The core resolves a resource's billable from the matching local provider-ID row. For a provider resource that has never been seen locally, bind ResolvesReconciliationBillables in the application and map provider data to a persisted model:
use Andriichuk\LaravelBilling\Contracts\ResolvesReconciliationBillables; $this->app->bind(ResolvesReconciliationBillables::class, AccountBillingResolver::class);
The resolver receives the driver name, model kind, and resource DTO. Return null when it cannot resolve the owner; reconciliation reports and skips that orphan without failing the sweep.
Subscription and transaction lifecycle events are emitted through the same synchronizer used by webhook projections. They include the persisted billing model and provider resource identity, so listeners can re-read authoritative state under their own lock. Events are dispatched only when projection attributes changed; --force explicitly replays them. --dry-run performs the same comparison but writes and dispatches nothing.
Events
Laravel lifecycle events are independent of provider event names:
WebhookReceived,WebhookProcessed,WebhookIgnored,WebhookFailedSubscriptionUpdatedTransactionUpdated
Drivers may normalize customer, subscription, transaction, renewal, payment-failure, refund, and chargeback events. Applications can listen to lifecycle events without depending on provider payload formats.
Provider escape hatches
- DTO results expose
rawProviderData()containing sanitized provider data. - Customer and subscription commands accept
providerOptions. Billing::driver('name')returns the underlying driver for provider-only APIs.- Payment methods are opaque
PaymentMethodReferencevalues; the core never normalizes or stores raw card, bank, or wallet payloads.
Security
Drivers must remove authorization headers, signature secrets, PANs, CVVs, and sensitive payment data before returning provider data or parsed webhook payloads. Never log raw credentials or payment instruments. Verify webhook signatures against the untouched request bytes, use HTTPS, isolate provider secrets in environment-backed configuration, and use a durable non-sync queue in production.
Development
composer install
composer format
composer format:test
composer test
composer analyse
composer check
composer validate --strict
The bundled fake driver records requests in memory, simulates failures, supports signed webhooks and reconciliation, and enables full lifecycle tests without network calls. External drivers can extend DriverComplianceTestCase; see docs/driver-authoring.md.
License
MIT. See LICENSE.md.