Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Stripe Laravel Package

jeffersongoncalves/laravel-stripe

A Laravel package that provides a convenient integration with Stripe, making it easy to configure API credentials and use Stripe's payment, customer, and subscription features in your Laravel applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This 1.0 release is a resource-oriented Stripe API client, not a Cashier-style billing layer. It talks to Stripe and hands you back arrays/objects — it does not ship migrations, Eloquent models, billable traits, or subscription state machines. Plan to own your own persistence and webhook handling; the package's job is to make the HTTP calls correct and typed.

Minimal path:

  1. Install and publish config (php artisan vendor:publish --tag=...), then put keys in .env — STRIPE_KEY / STRIPE_SECRET. Never commit live keys; keep stripe config out of anything that gets cached before env load (see Gotchas).
  2. Resolve the client from the container rather than constructing it by hand, so config and HTTP options stay in one place.
  3. Run stripe login && stripe listen --forward-to localhost:8000/webhooks/stripe before writing anything. You will need a real, verified webhook almost immediately — it is the fastest way to see what the API actually returns.

First use case — a one-off checkout, which exercises the most surface with the least code:

$client = app(StripeClient::class); // name per the README

$session = $client->checkout()->create([
    'mode' => 'payment',
    'line_items' => [['price' => $priceId, 'quantity' => 1]],
    'customer' => $customerId,           // create/lookup the customer first
    'success_url' => route('checkout.done').'?session_id={CHECKOUT_SESSION_ID}',
    'cancel_url' => route('checkout.cancel'),
]);
return redirect($session['url']);

Then a minimal /webhooks/stripe route that verifies the signature and dispatches a job. Don't build the success page's fulfilment logic on the redirect — build it on checkout.session.completed.

Where to look first: the webhook verification section (signature, replay window, rotation), then StripeException (you'll catch it in every controller), then the list/pagination conventions, which are uniform across resources.

Implementation Patterns

Wrap it, don't sprinkle it. The resource objects map 1:1 to Stripe's REST paths, so calling them from controllers spreads API shape across your app. Put intent-shaped methods on your own service (Billing::startSubscription($user, $priceId)) and let the package be the transport underneath. This is also your seam for faking in tests.

Treat webhooks as the source of truth, API responses as optimistic. A subscription created successfully can still fail its first invoice minutes later; invoice.payment_failed and customer.subscription.updated are what your database should trust. Pattern: every endpoint that mutates billing state also has a matching webhook handler, and handlers are idempotent on event.id (store processed IDs).

Webhook handler shape that scales:

public function __invoke(Request $request, StripeClient $stripe)
{
    $event = $stripe->webhooks()->verify(
        $request->getContent(),                 // raw body, not ->all()
        $request->header('Stripe-Signature')
    );

    ProcessStripeEvent::dispatch($event)->onQueue('billing'); // unique on $event['id']
    return response('', 200);
}

Verify, queue, ack. Do the work in a job so a slow handler can't cause Stripe to time out and retry into a duplicate.

Persist the mapping, not the payload. Store stripe_customer_id on users, stripe_subscription_id + status + current_period_end on a subscriptions table, stripe_price_id against your plan records. Pull the rest from Stripe when you need it. Amounts are integers in the currency's minor unit — use bigInteger, never floats.

Idempotency keys on every create/confirm. Generate one per logical operation (Str::uuid() stored on the order/job) and pass it through. This is what makes queue retries safe and is the single highest-value habit with this package.

Use expand to avoid N+1 over HTTP. Listing subscriptions and then fetching each customer is one round trip per row. Expand once: ['expand' => ['data.customer']]. Same for latest_invoice.payment_intent when rendering billing screens.

Pagination is cursor-based and uniform. limit (max 100) plus starting_after. Write one eachPage()-style helper in your service layer and reuse it across customers, invoices, events, subscriptions — the shapes are identical, so the abstraction is cheap here.

Checkout vs. Payment Intents. Prefer Checkout sessions for anything user-facing: Stripe hosts the card form, handles SCA, tax, and wallets, and you skip PCI scope. Reach for payment intents only for custom embedded UI or off-session charges — those are where confirm/capture/cancel and the redirect-return flow become your problem.

Integration tips. Register the webhook route outside web middleware (no CSRF, no session). Give yourself an artisan command that creates/reuses test products and prices so seeds don't hammer the live-mode-agnostic endpoints. And log the request_id from any failure — Stripe support will ask for it.

Gotchas and Tips

Http::fake() probably won't intercept this package. Most Stripe clients bring their own cURL/Guzzle transport, so Laravel's HTTP fake silently no-ops and your "unit" tests hit the network. Check the README for an injectable transport or client interface before you assume. If there isn't one, wrap the package behind your own interface and fake that — don't try to fake around it, and never let the suite run against live keys.

Signature verification needs the raw body. $request->all() or any middleware that re-encodes JSON will break the HMAC and give you a confusing "no matching signature" error. Read getContent(). If verification fails only in some environments, suspect a proxy rewriting the body, not the package.

The replay window and rotation-safety are features, but they have operational consequences. A replay window means server clock skew causes intermittent failures — keep NTP healthy. Rotation-safe verification means multiple signing secrets can be valid during a rollover; if you keep your own verification code as a fallback, make sure you try all configured secrets, not just the newest.

StripeException carries code, type, param, and Stripe's body — use them. param tells you exactly which field was rejected, which turns "invalid request" into a precise validation message. type distinguishes card_error (show the user, they can fix it) from api_error / rate_limit (retry or alert, the user can't). Catch it once at a boundary and map; don't let raw Stripe JSON reach a user.

Retry on 429 and 5xx with backoff — but only with idempotency keys. Stripe's limits are roughly 100 req/s live and 25 req/s test. Test-mode limits are the ones you'll actually hit in CI or a seeding script, and they look like random failures.

Products archive, prices can't be deleted. Only products have delete. Prices deactivate/archive. Any "delete plan" UI must be a soft operation, and you must keep historical prices around forever because old invoices reference them.

Cancel vs. resume is asymmetric. cancel defaults to at-period-end behavior with an immediate-cancel variant; resume only works before the period ends and throws once the subscription is fully canceled. Don't let your UI offer "resume" on a row whose status says otherwise — re-fetch instead of trusting cached state.

Lists don't return totals. There is no count on a list endpoint; a "you have 43 invoices" badge requires either has_more iteration or reading invoice.total_count style fields where they exist. Don't discover this in front of a customer.

Event filtering is single-type. "Filter by type" means one event type per request — subscribe to invoice.* style prefixes where supported, otherwise fan out into several calls or filter after fetching. Don't poll events for state; that path also has retention limits.

Config caching quirk. php artisan config:cache freezes env values at build time. If keys come from a secrets manager or are rotated per-environment at deploy, cache after the values are set, or resolve secrets at runtime in a service provider instead of baking them into config.

API version pinning. The client sends some Stripe-Version. Confirm which one is pinned and whether the package lets you override it; a silent major-version bump on a package upgrade can change response shapes (subscription period fields, invoice payment intent location) without touching your code. Pin deliberately, upgrade deliberately.

Extension points worth knowing: the config file (keys, API version, timeouts, retry policy), the webhook secret list for rotation, and whatever transport/client binding the package exposes. If a client instance is bound in the container, that's also your escape hatch for logging every request in staging or injecting a mock in tests — use it rather than editing vendor code.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings