Bonaventure OgetoBy Bonaventure Ogeto|

Build Paystack Subscriptions in Laravel

To build Paystack subscriptions in Laravel, create a plan via the API, initialize a transaction with the plan code, handle subscription webhooks with Jobs, track status in an Eloquent model, and use middleware to gate premium features behind active subscription checks.

Subscription Model

// database/migrations/create_subscriptions_table.php
Schema::create('subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('subscription_code')->unique();
    $table->string('plan_code');
    $table->string('email_token');
    $table->string('status')->default('active');
    $table->timestamp('next_payment_date')->nullable();
    $table->timestamps();
});

Subscribe a Customer

public function subscribe(Request $request)
{
    $response = Http::withToken(config('services.paystack.secret_key'))
        ->post('https://api.paystack.co/transaction/initialize', [
            'email' => auth()->user()->email,
            'plan' => $request->plan_code,
            'callback_url' => route('subscription.callback'),
        ]);

    $data = $response->json();

    if ($data['status'] ?? false) {
        return redirect($data['data']['authorization_url']);
    }

    return back()->with('error', $data['message'] ?? 'Failed');
}

Webhook Handlers

// In ProcessPaystackWebhook Job
case 'subscription.create':
    $user = \App\Models\User::where('email', $data['customer']['email'])->first();
    if ($user) {
        \App\Models\Subscription::updateOrCreate(
            ['user_id' => $user->id],
            [
                'subscription_code' => $data['subscription_code'],
                'plan_code' => $data['plan']['plan_code'],
                'email_token' => $data['email_token'] ?? '',
                'status' => 'active',
                'next_payment_date' => $data['next_payment_date'] ?? null,
            ]
        );
    }
    break;

case 'invoice.payment_failed':
    $subCode = $data['subscription']['subscription_code'] ?? null;
    if ($subCode) {
        \App\Models\Subscription::where('subscription_code', $subCode)
            ->update(['status' => 'past_due']);
    }
    break;

case 'subscription.disable':
    $subCode = $data['subscription_code'] ?? null;
    if ($subCode) {
        \App\Models\Subscription::where('subscription_code', $subCode)
            ->update(['status' => 'cancelled']);
    }
    break;

Subscription Middleware

// app/Http/Middleware/EnsureActiveSubscription.php
namespace App\Http\Middleware;

use Closure;

class EnsureActiveSubscription
{
    public function handle($request, Closure $next)
    {
        $user = $request->user();

        if (!$user || !$user->subscription || $user->subscription->status !== 'active') {
            return redirect('/subscribe');
        }

        return $next($request);
    }
}

// In routes:
Route::middleware(['auth', 'subscription'])->group(function () {
    Route::get('/premium', [PremiumController::class, 'index']);
});

Cancel Subscription

public function cancel()
{
    $sub = auth()->user()->subscription;

    if (!$sub) {
        return back()->with('error', 'No subscription found');
    }

    $response = Http::withToken(config('services.paystack.secret_key'))
        ->post('https://api.paystack.co/subscription/disable', [
            'code' => $sub->subscription_code,
            'token' => $sub->email_token,
        ]);

    if ($response->json()['status'] ?? false) {
        $sub->update(['status' => 'cancelled']);
        return redirect('/dashboard')->with('success', 'Subscription cancelled');
    }

    return back()->with('error', 'Cancellation failed');
}

Ship Payments Faster

Key Takeaways

  • Paystack manages recurring billing. You create plans, subscribe customers, and Paystack charges automatically.
  • Use Eloquent for subscription tracking. A Subscription model with status, subscription_code, and email_token.
  • Webhook events are the source of truth. Handle subscription.create, charge.success, and invoice.payment_failed.
  • Laravel middleware gates premium routes behind active subscription checks.
  • Store email_token from the subscription.create webhook for cancellation.
  • Dispatch Jobs from your webhook handler for reliable async processing.

Frequently Asked Questions

Can I use Laravel Cashier with Paystack?
No. Laravel Cashier is designed for Stripe and Paddle. For Paystack subscriptions, build the integration directly using the HTTP client and Paystack API as shown in this guide.
How do I handle plan upgrades?
Cancel the current subscription and create a new one on the new plan. Handle prorating in your application logic.
What happens when a renewal fails?
Paystack retries the charge. You receive invoice.payment_failed webhooks. Update the status to past_due and notify the customer.
How do I register the middleware?
Add it to your Kernel.php routeMiddleware array with a key like "subscription". Then use it in your route groups with middleware("subscription").

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon