Skip to content

Upgrading

The package is pre-1.0: the API and schema may change between minor versions. The full list is in the CHANGELOG.

  • New column billing_payments.voided_at (nullable timestamp), set by Billing::voidPayment(). Without it voiding fails, nothing else changes.

    Schema::table('billing_payments', function (Blueprint $table) {
    $table->timestamp('voided_at')->nullable()->after('failure_message');
    });
  • Billing::gateways()[...]['capabilities'] has a new voids key.

  • The webhook route is throttled and size-limited by default: webhook.rate_limit 600 requests per minute per gateway and IP (429 above it), webhook.max_payload_kb 1024 (413 above it). A throttle you added to webhook.middleware yourself still runs, after the built-in one. Set either to null to turn it off.
  • checkout_rate_limit is new and off by default — nothing changes until you set BILLING_CHECKOUT_RATE_LIMIT. A published config/billing.php needs no edits: the defaults apply without the new keys.
  • 0.13.1: the return route (billing.return) picks the page by the payment’s status instead of the URL’s {outcome}: a failed/canceled payment lands on return_urls.failed even when the gateway returned through success (Monobank, LiqPay, WayForPay, Hutko). A pending payment is polled once (ChecksPaymentStatus) before deciding. The final URL also carries status={status}.

  • New columns billing_payments.failure_code (nullable string(100)) and failure_message (nullable string(255)). Payments failed before the upgrade keep both null.

    Schema::table('billing_payments', function (Blueprint $table) {
    $table->string('failure_code', 100)->nullable()->after('paid_at');
    $table->string('failure_message')->nullable()->after('failure_code');
    });
  • billing.invoices.number_per_tenant (default true, unchanged behaviour). false numbers all tenants’ documents in one sequence.

  • 0.12.11: billing.pay no longer issues a checkout for an off-session charge waiting for its webhook (pending, with external_id, without payment_url); it redirects to return_urls.success instead.

  • 0.12.10: charge(), chargeWithMethod(), refund() and driver() throw BillingException instead of TypeError for a payment without a gateway.

  • 0.12.9: billing:reconcile-pending-payments measures age from updated_at instead of created_at — any update to a pending payment postpones its reconciliation by reconcile_after_minutes.

  • 0.12.8: Price::delete() and Plan::delete() throw BillingException when the price (or one of the plan’s prices) has subscriptions. Before, the database cascaded and deleted them. The published migration now creates billing_subscriptions.price_id with restrictOnDelete(); an existing database keeps the cascade, which still applies to deletes bypassing the models. To change it:

    Schema::table('billing_subscriptions', function (Blueprint $table) {
    $table->dropForeign(['price_id']);
    $table->foreign('price_id')->references('id')->on('billing_prices')->restrictOnDelete();
    });
  • 0.12.7: cancel() on a trial keeps it until trial_ends_at instead of cancelling now; resume() restarts a period that ran out during the pause.

  • 0.12.6: billing:health without an argument skips gateways with no secret credentials.

  • 0.12.5: package-managed subscriptions on a gateway without off-session charges (Paddle, custom drivers) enter dunning when their period ends instead of staying active. Check for such rows before upgrading — overdue ones go to past_due on the next run.

  • 0.12.4: a failed payment against an active subscription before its period ends no longer starts dunning.

  • 0.12.3: with a morph map, drivers now store the billable’s alias instead of its class name in billing_payment_methods.billable_type. Without a morph map nothing changes. With one, rows saved by attachPaymentMethod() before the upgrade still hold the class name and stay invisible to $billable->paymentMethods and renewals — rewrite them once:

    use Fomvasss\Billing\Models\PaymentMethod;
    use Illuminate\Database\Eloquent\Relations\Relation;
    foreach (Relation::morphMap() as $alias => $class) {
    PaymentMethod::where('billable_type', $class)->update(['billable_type' => $alias]);
    }

    If a billable then has two rows for the same card (one from a checkout, one attached by hand), delete the duplicate. On Stripe, a billable may also have extra customers created before the fix; they are harmless, the package reuses the customer of the billable’s first saved card.

  • 0.12.2: auto_invoice/auto_receipt issue nothing while the seller has no name.

  • 0.12.1: an invoice issued for an already paid payment no longer names the subscription’s (next) period.

  • issueInvoice() accepts a paid payment (the invoice comes out paid) instead of throwing.
  • New billing.invoices.auto_invoice and InvoiceItemsContract.
  • New optional group billing-migrations-invoices (billing_invoices, billing_document_sequences) and billing.invoices.* config — publish both and set BILLING_INVOICES_ENABLED=true to use them.

0.9 — Paddle, provider-managed subscriptions

Section titled “0.9 — Paddle, provider-managed subscriptions”
  • Stripe API version pinned to 2026-08-26.dahlia. Re-create an existing endpoint: php artisan billing:stripe-register-webhook --fresh, then store the new signing secret. The command warns when an endpoint’s version differs.
  • Re-run billing:stripe-register-webhook (without --fresh it now updates events in place) to subscribe to charge.refunded, invoice.paid and customer.subscription.*.
  • New columns: billing_subscriptions.provider_synced_at (nullable datetime) and an index on (gateway, external_id).
  • SubscriptionGatewayContract is deprecated in favour of StartsProviderSubscriptions + ManagesProviderSubscriptions.
  • cancel()/pause()/resume()/swapPlan() on a row with external_id now go to the driver and throw NotSupportedException when it can’t — they no longer change only the local row. The payment-outcome listener now skips such rows.
  • Custom signature validators should resolve credentials by $request->route('gateway') (and WebhookTenant::fromRequest()), not a hard-coded name.
  • Refund rows may be pending (Paddle) — filter $payment->refunds by status. refundableRemainder() reserves pending ones; Payment::recordRefundOf() takes a status.
  • Paddle setup: billing:paddle-register-webhook, the default payment link, domain approval.
  • New status SubscriptionStatus::Incomplete (no migration — plain string column). If your checkout created trialing rows with a past trial_ends_at, create them as incomplete instead.
  • 0.8.1: charge() resets a non-paid payment to pending when it issues a checkout.
  • TrialEnded event; billing:expire-trials expires trials row by row.
  • Every path into canceled goes through markCanceled(), which clears next_retry_at and grace_ends_at.
  • New column billing_payments.initiation (nullable string(20)).
  • 0.6.1: billing_payment_methods unique index is now (gateway, billable_type, billable_id, external_customer_id, external_id), and external_customer_id/external_id are varchar(191).
  • 0.6.2: tenant_id is derived from the billable on create. Backfill old rows yourself if reports need them.
  • New columns: billing_prices.quota_interval, quota_interval_count; billing_subscriptions.quota_period_ends_at.
  • New command billing:reset-usage-quotas (scheduled hourly).
  • 0.5.2: ReissueChargeOptionsContract (default unchanged).
  • isActive()/active() derive access from the row’s dates (trial end, cancels_at, due pause_ends_at) instead of waiting for the scheduler.
  • billing:expire-trials and the new billing:send-period-notices run hourly.
  • New columns: billing_prices.period_ending_notices, billing_subscriptions.period_notices_sent.
  • SubscriptionRenewed has previousStatus.
  • Hutko refunds via the API.
  • billing.retry_interval_hours / BILLING_RETRY_INTERVAL_HOURS replaced by billing.retry_intervals (a list); new column billing_prices.retry_intervals.
  • max_recurring_attempts defaults to 4 and counts attempts, not retries.
  • grace_ends_at = next_retry_at + grace_period_days.
  • An unparsable or non-positive interval throws.
  • RenewalChargeOptionsContract, billing.renewal.receipt_items for fiscal baskets on renewals.