Skip to main content

Overview

Stripe is a full-featured payment gateway that supports subscriptions, one-time payments, refunds, and more. Eufaturo Billing provides seamless Stripe integration through the eufaturo/billing-stripe package.
Stripe is the recommended gateway for most applications due to its comprehensive feature set, excellent documentation, and global reach.

Features

The Stripe integration supports: Subscription Management - Recurring billing with automatic charges ✅ One-Time Payments - Single purchases and orders ✅ Hosted Checkout - Stripe Checkout for payment collection ✅ Customer Portal - Self-service subscription management ✅ Refunds - Full and partial refund support ✅ Payment Methods - Cards, digital wallets (Apple Pay, Google Pay) ✅ Webhooks - Real-time event notifications ✅ Test Mode - Sandbox environment for development ✅ Proration - Automatic proration on plan changes ✅ Tax Calculation - Stripe Tax integration

Installation

Step 1: Install the Package

composer require eufaturo/billing-stripe
The package will auto-register via Laravel package discovery.

Step 2: Publish Configuration

php artisan vendor:publish --tag=billing-stripe-config
This creates config/billing-stripe.php:
return [
    // Stripe API keys
    'public_key' => env('STRIPE_PUBLIC_KEY'),
    'secret_key' => env('STRIPE_SECRET_KEY'),

    // Webhook configuration
    'webhook' => [
        'secret' => env('STRIPE_WEBHOOK_SECRET'),
        'tolerance' => 300, // seconds
    ],

    // Checkout configuration
    'checkout' => [
        'success_url' => env('STRIPE_SUCCESS_URL', '/checkout/success?session_id={CHECKOUT_SESSION_ID}'),
        'cancel_url' => env('STRIPE_CANCEL_URL', '/checkout/cancel'),
    ],

    // Customer portal
    'customer_portal' => [
        'enabled' => env('STRIPE_CUSTOMER_PORTAL_ENABLED', true),
    ],
];

Step 3: Configure Environment Variables

Add your Stripe credentials to .env:
# Get these from https://dashboard.stripe.com/apikeys
STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...

# Webhook signing secret (we'll configure this later)
STRIPE_WEBHOOK_SECRET=whsec_...

# Optional: Customize URLs
STRIPE_SUCCESS_URL=/billing/success?session_id={CHECKOUT_SESSION_ID}
STRIPE_CANCEL_URL=/billing/cancel
Never commit your secret key to version control! Use environment variables only.

Basic Usage

Getting the Stripe Gateway

use Eufaturo\Billing\Billing;

$stripe = Billing::gateways()->get('stripe');

Creating a Subscription

use Eufaturo\Billing\Billing;
use Eufaturo\Billing\Subscriptions\Enums\SubscriptionType;

// Get the plan
$plan = Billing::plans()->find('pro-monthly');

// Get Stripe gateway
$stripe = Billing::gateways()->get('stripe');

// Create subscription (gateway-managed)
$subscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $plan,
    type: SubscriptionType::PaymentGatewayManaged,
    paymentGateway: $stripe,
);

// Redirect to Stripe Checkout
return redirect($subscription->checkout_url);

One-Time Payment

// Create order
$order = Billing::orders()->create(
    billable: $user,
    productPlan: $plan,
    quantity: 1,
);

// Create checkout session
$checkout = $stripe->createPaymentCheckout(
    billable: $user,
    order: $order,
    successUrl: route('payment.success'),
    cancelUrl: route('payment.cancel'),
);

// Redirect to Stripe Checkout
return redirect($checkout->url);

Stripe Checkout Flow

1. Create Checkout Session

When a customer subscribes or makes a payment, create a Stripe Checkout session:
public function subscribe(Request $request)
{
    $user = auth()->user();
    $plan = Billing::plans()->find($request->input('plan'));
    $stripe = Billing::gateways()->get('stripe');

    // Create subscription
    $subscription = Billing::subscriptions()->create(
        billable: $user,
        plan: $plan,
        type: SubscriptionType::PaymentGatewayManaged,
        paymentGateway: $stripe,
    );

    // Redirect to Stripe Checkout
    return redirect($subscription->checkout_url);
}

2. Handle Success

After successful payment, Stripe redirects to your success URL:
Route::get('/checkout/success', function (Request $request) {
    $sessionId = $request->get('session_id');

    if (!$sessionId) {
        return redirect()->route('pricing')->with('error', 'Invalid session');
    }

    $stripe = Billing::gateways()->get('stripe');

    // Retrieve checkout session
    $session = $stripe->retrieveSubscriptionCheckout($sessionId);

    // Find subscription by metadata
    $subscription = Subscription::where('id', $session->metadata['subscription_id'])->firstOrFail();

    // Update subscription with Stripe data
    $subscription->update([
        'status' => SubscriptionStatus::Active,
    ]);

    // Store gateway record
    $subscription->gatewayRecords()->updateOrCreate(
        ['payment_gateway_id' => $stripe->id],
        ['external_id' => $session->id],
    );

    return redirect()->route('dashboard')->with('success', 'Welcome! Your subscription is now active.');
});

3. Handle Cancellation

If the customer cancels the checkout:
Route::get('/checkout/cancel', function () {
    return redirect()->route('pricing')->with('info', 'Checkout cancelled. You can try again anytime.');
});

Subscription Management

Cancel Subscription

use Eufaturo\Billing\Billing;

$subscription = $user->subscription();
$stripe = Billing::gateways()->get('stripe');

// Get Stripe subscription ID
$externalId = $subscription->getExternalIdForGateway($stripe);

// Cancel immediately
$stripe->cancelSubscription($externalId, immediately: true);

// Or cancel at period end
$stripe->cancelSubscription($externalId, immediately: false);

// Update local subscription
$subscription->update([
    'status' => SubscriptionStatus::Cancelled,
    'ends_at' => now(),
]);

Resume Subscription

// Resume a subscription cancelled at period end
if ($subscription->status === SubscriptionStatus::Cancelled && $subscription->ends_at->isFuture()) {
    $externalId = $subscription->getExternalIdForGateway($stripe);

    $stripe->resumeSubscription($externalId);

    $subscription->update([
        'status' => SubscriptionStatus::Active,
        'ends_at' => null,
    ]);
}

Change Plan

$newPlan = Billing::plans()->find('enterprise-monthly');
$externalId = $subscription->getExternalIdForGateway($stripe);

// Stripe handles proration automatically
$stripe->updatePlan($externalId, $newPlan, prorate: true);

// Update local subscription
$subscription->update([
    'product_plan_id' => $newPlan->id,
    'price' => $newPlan->price,
]);

Update Payment Method

// Get new payment method ID from Stripe.js
$paymentMethodId = $request->input('payment_method_id');

$externalId = $subscription->getExternalIdForGateway($stripe);

$stripe->updatePaymentMethod($externalId, $paymentMethodId);

Webhooks

Webhooks allow Stripe to notify your application of events in real-time.

Step 1: Configure Webhook Endpoint

The Stripe package automatically registers a webhook route:
POST /webhooks/stripe

Step 2: Create Webhook in Stripe Dashboard

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Click Add endpoint
  3. Enter your webhook URL:
    https://your domain.com/webhooks/stripe
    
  4. Select events to listen for:
    • checkout.session.completed
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.paid
    • invoice.payment_failed
    • payment_intent.succeeded
    • payment_intent.payment_failed
  5. Click Add endpoint
  6. Copy the Signing secret (starts with whsec_)

Step 3: Add Signing Secret to .env

STRIPE_WEBHOOK_SECRET=whsec_...

Step 4: Webhook Events

The Stripe integration automatically handles these events:

checkout.session.completed

Fired when a customer completes checkout:
// Automatically handled by StripeWebhookHandler
// - Activates subscription
// - Creates gateway record
// - Records transaction

customer.subscription.updated

Fired when subscription changes:
// Automatically handled
// - Updates subscription status
// - Updates plan if changed
// - Syncs trial dates

customer.subscription.deleted

Fired when subscription is cancelled:
// Automatically handled
// - Marks subscription as cancelled
// - Sets ends_at date

invoice.paid

Fired when invoice is successfully paid:
// Automatically handled
// - Records transaction
// - Updates subscription status

invoice.payment_failed

Fired when payment fails:
// Automatically handled
// - Marks subscription as past_due
// - Records failed transaction
// - Triggers notification

Custom Webhook Handling

You can listen to Stripe events in your own code:
use Eufaturo\BillingStripe\Events\StripeWebhookReceived;

Event::listen(StripeWebhookReceived::class, function ($event) {
    if ($event->type === 'customer.subscription.updated') {
        $subscription = $event->data['object'];

        // Your custom logic here
        Log::info('Subscription updated', ['id' => $subscription['id']]);
    }
});

Testing

Test Mode

Stripe provides a test environment with test API keys:
# Use test keys for development
STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...

Test Card Numbers

Use these test cards in Stripe Checkout:
Card NumberDescription
4242424242424242Successful payment
4000000000000002Card declined
4000002500003155Requires authentication (3D Secure)
4000000000009995Insufficient funds
Expiry: Any future date CVC: Any 3 digits Postal Code: Any 5 digits Full list: https://stripe.com/docs/testing

Testing Webhooks Locally

Use Stripe CLI to forward webhooks to your local environment:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Forward webhooks
stripe listen --forward-to localhost:8000/webhooks/stripe
The CLI will output a webhook signing secret. Add it to .env:
STRIPE_WEBHOOK_SECRET=whsec_...

Triggering Test Events

# Trigger a successful payment
stripe trigger checkout.session.completed

# Trigger a failed payment
stripe trigger invoice.payment_failed

# Trigger subscription cancellation
stripe trigger customer.subscription.deleted

Customer Portal

Stripe Customer Portal allows customers to manage their subscriptions self-service.

Enable Customer Portal

  1. Go to Stripe Dashboard → Settings → Billing → Customer portal
  2. Configure portal settings:
    • Allow customers to update payment methods
    • Allow customers to cancel subscriptions
    • Configure cancellation behavior (immediately vs. end of period)
use Stripe\StripeClient;

public function customerPortal()
{
    $user = auth()->user();
    $stripe = new StripeClient(config('billing-stripe.secret_key'));

    // Get Stripe customer ID
    $stripeGateway = Billing::gateways()->get('stripe');
    $customerRecord = $user->getGatewayRecordForGateway($stripeGateway);

    // Create portal session
    $session = $stripe->billingPortal->sessions->create([
        'customer' => $customerRecord->external_id,
        'return_url' => route('billing.settings'),
    ]);

    return redirect($session->url);
}
{{-- In your billing settings view --}}
<a href="{{ route('customer-portal') }}" class="btn">
    Manage Subscription
</a>

Advanced Features

Metadata

Attach custom data to Stripe objects:
$checkout = $stripe->createSubscriptionCheckout(
    billable: $user,
    plan: $plan,
    successUrl: route('checkout.success'),
    cancelUrl: route('checkout.cancel'),
    metadata: [
        'user_id' => $user->id,
        'plan_id' => $plan->id,
        'source' => 'marketing_campaign_2024',
    ],
);
Metadata is returned in webhooks and can be used for tracking.

Trial Periods

$subscription = Billing::subscriptions()->create(
    billable: $user,
    plan: $plan,
    type: SubscriptionType::PaymentGatewayManaged,
    paymentGateway: $stripe,
    trialEnds: now()->addDays(14), // 14-day trial
);
Stripe won’t charge until the trial ends.

Coupons and Discounts

// Create Stripe coupon
$stripe = new StripeClient(config('billing-stripe.secret_key'));

$coupon = $stripe->coupons->create([
    'percent_off' => 20,
    'duration' => 'forever',
    'id' => 'SAVE20',
]);

// Apply during checkout
$checkout = $stripe->createSubscriptionCheckout(
    billable: $user,
    plan: $plan,
    successUrl: route('checkout.success'),
    cancelUrl: route('checkout.cancel'),
    metadata: ['coupon' => 'SAVE20'],
);

Stripe Tax

Enable automatic tax calculation:
  1. Go to Stripe Dashboard → Settings → Tax
  2. Enable Stripe Tax
  3. Tax will be calculated automatically during checkout

Troubleshooting

”No such customer” Error

Problem: Stripe customer doesn’t exist. Solution:
// Ensure customer is created before creating subscription
$customer = $stripe->createOrRetrieveCustomer($user);

Webhook Signature Verification Failed

Problem: Webhook signature doesn’t match. Solutions:
  1. Verify STRIPE_WEBHOOK_SECRET is correct
  2. Check webhook endpoint URL is correct in Stripe Dashboard
  3. Ensure webhook route is not behind CSRF protection:
// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    'webhooks/*',
];

Test Cards Not Working

Problem: Test cards declined in test mode. Solutions:
  1. Verify you’re using test API keys (pk_test_ and sk_test_)
  2. Use correct test card numbers from Stripe documentation
  3. Check Stripe Dashboard for error messages

Subscription Not Activating

Problem: Subscription stays in “new” status after payment. Solutions:
  1. Verify webhook is configured and receiving events
  2. Check webhook signing secret is correct
  3. Look for errors in webhook logs (Stripe Dashboard → Developers → Webhooks → Your endpoint)

Best Practices

✅ Do

  • Use test mode during development
    STRIPE_PUBLIC_KEY=pk_test_...
    STRIPE_SECRET_KEY=sk_test_...
    
  • Verify webhooks before processing
    // Automatically handled by StripeWebhookHandler
    
  • Handle failed payments gracefully
    Event::listen(StripeInvoicePaymentFailed::class, function ($event) {
        // Notify user, retry payment, etc.
    });
    
  • Store customer IDs for future reference
    $user->gatewayRecords()->create([
        'payment_gateway_id' => $stripe->id,
        'external_id' => $customer->id,
    ]);
    

❌ Don’t

  • Don’t skip webhook verification - always verify signatures
  • Don’t commit API keys to version control
  • Don’t rely on redirects for critical updates - use webhooks
  • Don’t create duplicate customers - check for existing first

Next Steps


See Also