jiannius/stripe

Jiannius Stripe Wrapper

Maintainers

Package info

github.com/jiannius/stripe

pkg:composer/jiannius/stripe

Statistics

Installs: 633

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.2 2026-05-13 07:13 UTC

This package is auto-updated.

Last update: 2026-05-13 07:14:58 UTC


README

A thin Laravel wrapper around the official Stripe PHP SDK. Handles Checkout Sessions, subscription webhooks, and signature verification with sensible defaults.

Requirements

  • PHP 8.2+ (8.3+ for Laravel 13)
  • Laravel 10, 11, 12, or 13
  • stripe/stripe-php 10 through 17

Installation

composer require jiannius/stripe:^1.2

The service provider is auto-discovered via extra.laravel.providers.

Configuration

Add Stripe keys to config/services.php:

'stripe' => [
    'public_key'     => env('STRIPE_PUBLIC_KEY'),
    'secret_key'     => env('STRIPE_SECRET_KEY'),
    'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],

And to .env:

STRIPE_PUBLIC_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

Never commit live keys to source control. Use environment variables or a secrets vault. For production traffic, prefer a restricted API key (rk_*) over a full secret key.

Implement the host controller

The package registers three routes that point at App\Http\Controllers\StripeController. You must implement this controller in your application — the package supplies the routes and contract, your app supplies the behavior.

Route Name Method Purpose
/__stripe/success __stripe.success GET Stripe redirects users here after payment
/__stripe/cancel __stripe.cancel GET Stripe redirects users here on cancel
/__stripe/webhook __stripe.webhook POST Stripe posts events here

Minimal controller:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class StripeController extends Controller
{
    public function success(Request $request) { /* ... your business logic ... */ }
    public function cancel(Request $request)  { /* ... your business logic ... */ }
    public function webhook(Request $request) { /* see "Handling webhooks" below */ }
}

Register your webhook endpoint with Stripe

Once your app is deployed and the controller is in place, register the webhook endpoint:

$webhookSecret = app('stripe')->createWebhook();

This deletes any existing endpoint pointing at the same URL, creates a fresh one subscribed to the events the package handles, and returns the new signing secret. Store it as STRIPE_WEBHOOK_SECRET — every call to createWebhook() rotates the secret.

Events subscribed:

  • checkout.session.completed
  • checkout.session.expired
  • checkout.session.async_payment_succeeded
  • checkout.session.async_payment_failed
  • invoice.paid
  • invoice.payment_failed

If you'd rather manage the endpoint manually in the Stripe Dashboard, enable the same six events.

Creating a Checkout session

return app('stripe')->checkout([
    'mode'     => 'subscription',
    'customer' => $user->stripe_customer_id,
    'metadata' => ['order_id' => $order->id],
    'line_items' => [[
        'quantity' => 1,
        'price_data' => [
            'currency' => 'USD',
            'product_data' => ['name' => 'Pro Plan'],
            'unit_amount' => '15.00',
            'recurring' => ['interval' => 'month', 'interval_count' => 1],
        ],
    ]],
]);

Returns a Laravel RedirectResponse to the Stripe-hosted checkout page.

unit_amount accepts two forms

Input Result Notes
"15.00", "15.5" 1500, 1550 Decimal strings are multiplied by 100
1500, "1500" 1500 Integer (smallest currency unit) passed through

For zero-decimal currencies (JPY, KRW, etc.), pass an integer — multiplying yen by 100 would be wrong.

Using an existing Stripe Price ID

Pass price instead of price_data and the package leaves the line item untouched:

'line_items' => [[
    'quantity' => 1,
    'price'    => 'price_xxx',
]],

Metadata flows into success/cancel URLs

Anything you pass under metadata is forwarded as route parameters on the __stripe.success and __stripe.cancel URLs, so your controller can read them off the request.

Handling webhooks

public function webhook(Request $request)
{
    $stripe = app('stripe');
    $status = $stripe->getWebhookStatus();

    if ($status === null) {
        return response('Invalid signature', 400);
    }

    $event   = $stripe->getValidatedEvent();   // \Stripe\Event
    $object  = $event->data->object;           // \Stripe\Invoice, CheckoutSession, ...

    if (\App\Models\ProcessedWebhook::where('event_id', $event->id)->exists()) {
        return response('Already processed', 200);
    }

    match ($status) {
        'success'       => /* one-time / first-period payment succeeded */,
        'renew-success' => /* subscription renewed for another period */,
        'renew-failed'  => /* renewal payment failed; Stripe may retry */,
        'processing'    => /* async payment method still settling */,
        'failed'        => /* checkout expired or async payment failed */,
    };

    \App\Models\ProcessedWebhook::create(['event_id' => $event->id]);

    return response('OK', 200);
}

Status reference

Status Triggered by Meaning
success checkout.session.completed (paid), checkout.session.async_payment_succeeded, invoice.paid (non-cycle) One-time payment or first subscription invoice succeeded
renew-success invoice.paid with billing_reason=subscription_cycle Subscription renewed at period end
renew-failed invoice.payment_failed with billing_reason=subscription_cycle Renewal payment failed (Stripe may retry per your dunning settings)
processing checkout.session.completed with payment_status != paid Customer used an async method (e.g. ACH) — money hasn't cleared yet
failed checkout.session.expired, checkout.session.async_payment_failed Checkout expired or the async payment failed
null Signature mismatch / malformed payload Treat as untrusted; return a 4xx and do not process

Idempotency

Stripe retries failed webhook deliveries for up to 3 days. Always dedupe by $event->id (format evt_xxx) before acting — otherwise a single renewal can mark a customer's account renewed multiple times. Smart Retries can also legitimately send a renew-failed followed by a renew-success for the same invoice; your handler should treat them as a sequence rather than as conflicting outcomes.

Accessing the validated event vs. the raw payload

Method Returns When to use
getWebhookStatus() string|null Classify the event into a small set of statuses
getValidatedEvent() \Stripe\Event|null Typed access to event/invoice/subscription/customer IDs and any field
getWebhookPayload() array|null (unvalidated) Debugging only. Do not act on this without also calling validateWebhookPayload() or getValidatedEvent().

All three share an internal cache — the signature is verified once per request.

Cancelling subscriptions

app('stripe')->cancelSubscription($subscriptionId);

Cancels immediately. To cancel at period end (so the customer keeps access until the period they've already paid for ends), call the SDK directly:

app('stripe')->getStripeClient()->subscriptions->update($subscriptionId, [
    'cancel_at_period_end' => true,
]);

Runtime configuration

For multi-tenant apps that need to swap Stripe accounts at runtime, override the configured keys before any call:

app('stripe')
    ->setSecretKey($tenant->stripe_secret_key)
    ->setPublicKey($tenant->stripe_public_key)
    ->setWebhookSecret($tenant->stripe_webhook_secret)
    ->checkout([ /* ... */ ]);

Settings cascade: explicit setters take precedence over config('services.stripe.*').

Testing connectivity

['success' => $ok, 'error' => $message] = app('stripe')->test();

Calls accounts->all() on the configured secret key. Useful as a settings-page health check.

Security checklist

  • Store STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in environment variables or a secrets vault, never in source.
  • Use a restricted API key (rk_*) in place of the full secret key.
  • Add a pre-commit hook to block sk_live_ / rk_live_ patterns.
  • Use separate keys per environment (production / staging / local).
  • Rotate keys when teammates with access leave.
  • Optionally allowlist Stripe's webhook IPs on your edge as defense in depth.

Versioning

Tag Laravel stripe-php Notes
v0.1 10 ^10.0 Pre-Laravel 13 baseline
v1.0 10–13 ^10.0 Laravel 13 dependency widening
v1.1 10–13 >=10.0 <18.0 Webhook + checkout bug fixes; widened stripe-php
v1.2 10–13 >=10.0 <18.0 Adds getValidatedEvent(); consolidated webhook event parsing

Upgrading from v0.1 / v1.0

No code changes required — every public method is preserved. Two things to do after upgrading:

  1. Re-register the webhook so the renewal events are delivered:
    $newSecret = app('stripe')->createWebhook();
    // update STRIPE_WEBHOOK_SECRET in your environment
    Or in the Stripe Dashboard, add invoice.paid and invoice.payment_failed to the existing endpoint.
  2. Adopt getValidatedEvent() in your webhook controller to drop the second JSON parse and to get the event ID for idempotency.

License

MIT.