Skip to main content

Dunning

When an invoice payment fails, Subscribd automatically retries the charge according to a configurable dunning schedule. Subscriptions that exhaust all retries without a successful payment are marked expired and access is revoked.

Default retry schedule

Out of the box, Subscribd retries failed payments at:
  • 24 hours after initial failure
  • 72 hours after initial failure
  • 168 hours (7 days) after initial failure
If all three retries fail, the subscription moves to expired and the SubscriptionExpired event is fired.

Configuration

// config/subscribd.php
'dunning' => [
    'strategy'       => \Pixelworxio\Subscribd\Dunning\ConfigDrivenDunningStrategy::class,
    'retry_after'    => [24, 72, 168],   // hours after initial failure
    'notify_at'      => [72, 120],       // hours — fires PaymentRetryFailed at these points
    'grace_days'     => env('SUBSCRIBD_GRACE_PERIOD_DAYS', 3),
    'cancel_on_fail' => env('SUBSCRIBD_CANCEL_ON_DUNNING_FAIL', true),
],
The retry_after array defines when retries are attempted, in hours after the first failure. You can add or remove entries to suit your billing cycle. For example, a weekly billing cycle might use [24, 96, 240].

How dunning works

  1. Payment fails — gateway fires invoice.payment_failed (or equivalent). The subscription moves to past_due.
  2. Retry job scheduled — a RetryInvoice job is queued for each interval in retry_after.
  3. Retry succeeds — the invoice is marked paid, the subscription returns to active, and InvoicePaid is fired.
  4. Retry failsPaymentRetryFailed is fired if the current hour matches a notify_at threshold.
  5. All retries exhausted — if cancel_on_fail is true (default), the subscription is marked expired and SubscriptionExpired is fired.

Custom dunning strategy

Implement Pixelworxio\Subscribd\Contracts\DunningStrategy to replace the built-in schedule with custom logic:
<?php

namespace App\Billing;

use Pixelworxio\Subscribd\Contracts\DunningStrategy;
use Pixelworxio\Subscribd\Models\Invoice;

class MyDunningStrategy implements DunningStrategy
{
    /**
     * Return retry delay in hours for the given attempt number (1-indexed).
     * Return null to stop retrying.
     */
    public function retryAfterHours(Invoice $invoice, int $attempt): ?int
    {
        return match ($attempt) {
            1 => 24,
            2 => 72,
            3 => 168,
            default => null,
        };
    }

    public function shouldCancelOnFailure(Invoice $invoice): bool
    {
        return true;
    }
}
Register your strategy in config/subscribd.php:
'dunning' => [
    'strategy' => \App\Billing\MyDunningStrategy::class,
],

Notifying customers during dunning

Listen to PaymentRetryFailed to send reminder emails or in-app notifications:
use Pixelworxio\Subscribd\Events\PaymentRetryFailed;

class SendPaymentRetryFailedNotification
{
    public function handle(PaymentRetryFailed $event): void
    {
        $event->subscription->billable->notify(new PaymentActionRequiredNotification($event->invoice));
    }
}
Register your listener in AppServiceProvider::boot():
Event::listen(PaymentRetryFailed::class, SendPaymentRetryFailedNotification::class);

Manually retrying an invoice

Retry a specific invoice immediately, bypassing the dunning schedule:
use Pixelworxio\Subscribd\Actions\RetryInvoice;

app(RetryInvoice::class)->execute($invoice);
This attempts a charge on the customer’s default payment method and returns the updated Invoice model. If the charge succeeds, the subscription returns to active.

Grace period

When a subscription enters past_due, the billable retains access for grace_days (default: 3) before the subscription is expired. Configure this globally or per plan:
SUBSCRIBD_GRACE_PERIOD_DAYS=5
Or in config/subscribd.php:
'dunning' => [
    'grace_days' => 5,
],

Updating payment methods

When a customer updates their payment method during a past_due period, trigger an immediate retry:
use Pixelworxio\Subscribd\Actions\RetryInvoice;

// After payment method updated — surface the failed invoice and retry
$invoice = $user->invoices()->failed()->latest()->first();

if ($invoice) {
    app(RetryInvoice::class)->execute($invoice);
}

Checking dunning status

$user->subscription()->status->value; // 'past_due'

$user->pastDue();   // true while in dunning
$user->subscribed(); // true until grace_days expires

Next steps

  • Hooks and Events — Events fired during the dunning cycle
  • Webhooks — Receiving payment failure events from gateways