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

Payum Stripe Laravel Package

flux-se/payum-stripe

flux-se/payum-stripe is a Payum gateway for Stripe that lets Laravel/PHP apps process Stripe payments through a consistent abstraction. It supports card payments, Payment Intents, and webhook handling for common checkout flows.

View on GitHub
Deep Wiki
Context7

Getting Started

This package provides Payum gateway factories that wrap the official stripe/stripe-php SDK, so you can plug Stripe into any framework already using Payum (Sylius, custom Laravel apps using payum/payum, etc.). It is not a standalone Stripe client — everything flows through Payum's Gateway, Token, and action pipeline.

Install:

composer require flux-se/payum-stripe:^2.1

You also need payum/payum and a PSR container with payum/iso4217 (or sylius/currency) to resolve currencies.

Note on the SDK since v2.1.0: the package no longer pins a single stripe/stripe-php major — it declares support for multiple Stripe SDK versions, so Composer is free to resolve a newer stripe-php than you previously had. After upgrading, run composer show stripe/stripe-php and composer update stripe/stripe-php --dry-run so you know exactly which SDK version you're actually running; that version is now a variable in bug reports, not a constant.

Docs/issues live under the Sylius org. The GitHub home is Sylius/PayumStripe (changelogs, PRs, release tags) even though you install via Composer — check the tag list there, not the Packagist metadata, when validating a version.

Register the factory with Payum builder. The exact factory class you pick determines the checkout flow — pick one and commit to it:

use Payum\Core\PayumBuilder;
use FluxSE\PayumStripe\StripeCheckoutSessionGatewayFactory;
// or StripeCheckoutGatewayFactory, StripeJsGatewayFactory, StripeElementsGatewayFactory

$payum = (new PayumBuilder())
    ->addDefaultStorages()
    ->addGatewayFactory(new StripeCheckoutSessionGatewayFactory())
    ->addGateway('stripe', [
        'factory' => 'stripe_checkout_session',
        'publishable_key' => env('STRIPE_PUBLISHABLE_KEY'),
        'secret_key'      => env('STRIPE_SECRET_KEY'),
        'webhook_secret'  => env('STRIPE_WEBHOOK_SECRET'), // required for webhooks
    ])
    ->getPayum();

First use case — a Stripe Checkout Session redirect:

$payment = new Payment(); // your Payum Payment model
$payment->setNumber('order-123');
$payment->setTotalAmount(1999); // minor units
$payment->setCurrencyCode('USD');
$payment->setDetails(['success_url' => 'https://app.test/success', 'cancel_url' => 'https://app.test/cancel']);

$storage = $payum->getStorage(Payment::class);
$storage->update($payment);

$captureToken = $payum->getTokenFactory()->createCaptureToken(
    'stripe', $payment, 'https://app.test/payment/done'
);
header('Location: ' . $captureToken->getTargetUrl());

Look first at: the Action/ directory inside the package (each gateway has a CaptureAction, ConvertPaymentAction, StatusAction, and NotifyAction/webhook action). Those four classes define the entire lifecycle and are what you'll subclass or debug when something breaks. Second, read the factory's default config array to see which Stripe options are exposed (capture_method, payment_method_types, line_items, etc.).

Implementation Patterns

Choose the right factory and don't mix them. Each factory produces a Token with a different expected behavior:

  • stripe_checkout_session — modern Stripe-hosted page, offsite redirect, best for new builds and SCA compliance.
  • stripe_checkout — legacy Checkout, keep only for migration-compatibility.
  • stripe_elements / stripe_js — on-site card form using Stripe.js; the package's StatusAction expects a returned payment_intent or token in request data, which you must POST from the front end.

The canonical flow is: prepare → redirect user → user completes on Stripe or your page → Stripe webhook calls NotifyToken → webhook action updates the Payum Payment details → user is redirected to success_url → you call StatusAction (via Payum\Http\RequestVerifier or getTokenFactory()->createToken) to reconcile state. Treat the redirect as untrusted and the webhook as authoritative.

Retrying a failed card on Elements/JS (changed in v2.1.0). If a PaymentIntent is still payable (requires_payment_method, requires_confirmation, or after a decline), reusing it now causes the Stripe.js Elements form to be re-rendered rather than being treated as a finished flow. That's what you want for the "card declined, try again" path: re-execute the capture token / re-render the page against the same Payment record and the customer gets a usable form instead of a dead page. Do not lean on this for succeeded or canceled intents — those still need the webhook/StatusAction reconciliation, and reusing a consumed intent server-side will not magically produce a payable form. Treat re-render as a UX convenience, never as a state transition.

Wiring webhooks: generate a NotifyToken per payment and register its URL in Stripe:

$notifyToken = $payum->getTokenFactory()->createNotifyToken('stripe', $payment);
// https://app.test/payment/notify/<hash>

Route that URL to $payum->getHttpRequestVerifier()->verify($request) + $payum->getGateway($token->getGatewayName())->execute($token). The package's webhook action verifies the Stripe-Signature header against webhook_secret, so this config key is effectively mandatory in production — a missing secret usually means the action throws or is silently skipped depending on version.

Sylius integration. If you're on Sylius, register the factory in your plugin's PaymentMethod gateway config and expose publishable_key/secret_key as PaymentMethod config fields rather than app-wide env vars, so each channel can have its own Stripe account. This is how the package is most commonly consumed — check Sylius docs on "custom payment gateway" for the wiring. Given the package now lives under the Sylius org and is co-maintained by Sylius team members, Sylius-first wiring is the best-supported path; expect Laravel-side usage to be more DIY.

Refunds and captures. RefundAction and CaptureAction map onto the Stripe PHP SDK methods refunds->create and paymentIntents->capture. For two-step auth/capture (cards only), set capture_method: manual in gateway config and expect the Payment to be authorized after the redirect, captured only after you call capture. Note that many Stripe methods (iDEAL, SEPA, Klarna) do not support separate capture.

Customizing without forking. Each factory exposes a config array merged into the Stripe request. Prefer changing config over subclassing. When you must change behavior, subclass the relevant action and register it before the default by overriding StripeCheckoutSessionGatewayFactory::createConfig() in your own factory that calls the parent and then reorders actions — Payum executes actions in order, and later actions typically no-op if $request->isMarkedAsDone().

Testing locally. Use Stripe test keys and stripe listen --forward-to (or ngrok) to tunnel webhooks. Assert on Payment::getDetails() — the package stashes the payment_intent id, client_secret, and session id there, and that details array is your only source of truth when debugging.

Gotchas and Tips

Amounts are minor units, currencies are strings. Payum's Payment::getTotalAmount() is decimal in the model, but the Stripe SDK expects the smallest currency unit (1999 = $19.99). The package's ConvertPaymentAction handles this for the payment amount — but if you construct line_items, amount_off, or refund amounts manually, you must multiply yourself and beware of zero-decimal currencies (JPY, KRW) where the multiplier is 1, not 100. Use payum/iso4217 or Money\Money to avoid the classic "100x refund" bug.

A widened SDK range is not the same as "tested against everything." v2.1.0's multi-SDK support means a fresh composer update can silently change PaymentIntent request/response shapes underneath you. Pin this package to ^2.1 and commit your composer.lock; when you do bump stripe/stripe-php, run your webhook + capture integration tests before deploying. Report bugs with the resolved SDK version included — the maintainers now need that to reproduce.

Upgrading to 2.1.0 is a minor bump with one behavioral change. The Elements/JS re-render on still-payable PaymentIntents changes what your front end sees after a decline. If you had a custom "declined → build a brand new Payment from scratch" workaround, re-test it; the package may now re-render the existing intent, and you could end up with two live intents for one order. Verify your Payum Payment is the single source of truth for which intent is current.

Webhook signature failures look like silence. In several Payum action implementations, a SignatureVerificationException is wrapped and the action simply returns without marking the request as done, so the Payment stays pending forever. When a webhook "doesn't work," first check that webhook_secret is set, then that the raw request body wasn't consumed by a middleware (Laravel's TrimStrings, Symfony's validate middleware, or anything reading php://input before Payum) — signature verification needs the exact bytes Stripe sent.

Idempotency is not free. Stripe retries webhooks for up to three days. Payum's token model naturally makes notify handlers near-idempotent, but if your success_url also calls capture/execute, you'll double-process. Guard with your own payment.status === 'captured' check or by comparing the Stripe PaymentIntent.status before mutating your domain model. Set an Idempotency-Key on any outbound capture/refund call — the SDK accepts it as an option array on create(...) but the package does not set one for you.

StatusAction versus reality. Stripe's PaymentIntent has many states (requires_payment_method, requires_action, processing, succeeded, canceled), while Payum reasons in new/pending/authorized/captured/failed. The mapping in this package is lossy — processing typically lands as pending, which is correct but means your success page must poll or wait for the webhook rather than assume captured. Do not treat a redirect back to success_url as proof of payment, and remember that a re-rendered Elements form (v2.1.0) means the intent is explicitly not done.

Config quirks per factory. Each factory ships its own default config keys; stripe_checkout_session has options like line_items, mode, success_url, cancel_url, while stripe_js historically reads payment_intent from the request. Copying config keys between gateways is a common source of "silently ignored" settings — check the factory's createConfig() in the source before assuming a key is honored. Success/cancel URLs can be set per-payment via Payment::setDetails() and will override gateway defaults only on some factories (check the ConvertPaymentAction).

Extension points. The cleanest hooks are: (1) your own Action classes registered on a custom gateway factory; (2) a custom GatewayFactory extending the package's, overriding createConfig(); (3) a Payum\Core\Extension\Extension middleware that runs before CaptureAction/NotifyAction — useful for logging, idempotency, or attaching metadata like order_id. Prefer metadata over description for anything you'll grep later in the Stripe dashboard.

Maintenance reality check. This is a thin shim under active but low-volume stewardship: it now carries Sylius org branding and Sylius-team contributors (v2.1.0 is the first release from that contributor set), which is a positive signal for long-term survival, but the dependent/user base is still small. Treat it as a shim, not a framework. Pin the package to a specific tag or ^2.1, keep composer.lock committed, and skim the changelog on the Sylius/PayumStripe repo for the version you install rather than trusting Packagist metadata dates. Budget time for a fork if Stripe makes a breaking API change, and write integration tests against the Stripe \Stripe\HttpClient\MockClient so an upstream change (or an unexpected SDK version resolution) fails loudly rather than in production.

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.
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
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin