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 Bundle Laravel Package

flux-se/payum-stripe-bundle

Integrates the Stripe payment gateway into Symfony applications via Payum, providing gateway factory configuration and actions for handling Stripe payments, captures, and refunds through a unified API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register the bundle:

composer require flux-se/payum-stripe-bundle:^3.0.1
// config/bundles.php
return [
    // ...
    FluxSE\PayumStripeBundle\FluxSEPayumStripeBundle::class => ['all' => true],
];

Note: the upstream repository is FLUX-SE/PayumStripeBundle and is still shipping releases (v3.0.1 added Symfony 8 support). A Sylius-maintained fork also exists at Sylius/PayumStripeBundle. Whichever you pulled, the class is <Vendor>\PayumStripeBundle\<Vendor>PayumStripeBundle — check vendor/.../src/*Bundle.php to know which one you actually have before editing bundles.php.

Symfony 8 requires v3.0.1 or later; v3.0.0 and earlier will fail dependency resolution on Symfony 8. On Symfony 6.4/7.x you are free to pin v3.0.0. Confirm with composer show flux-se/payum-stripe-bundle and check that the Payum core constraint resolves for your Symfony version.

Put credentials in env — never in YAML:

STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

Then register the Payum gateway:

# config/packages/payum.yaml
payum:
    gateways:
        stripe_checkout_session:
            factory: stripe_checkout_session
            publishable_key: '%env(STRIPE_PUBLISHABLE_KEY)%'
            secret_key: '%env(STRIPE_SECRET_KEY)%'
            webhook_secret: '%env(STRIPE_WEBHOOK_SECRET)%'

Confirm the factory is registered before going further — a typo here fails later as an opaque RequestNotSupported:

bin/console debug:container --tag=payum.gateway_factory | grep -i stripe
bin/console debug:router | grep -i payum

Where to look first (in this order):

  1. The repo README's "Stripe Dashboard / webhook" section — this is the step people skip and then spend an afternoon on.
  2. src/DependencyInjection/Configuration.php — the authoritative list of gateway options.
  3. src/Action/CaptureAction.php (builds the Checkout Session) and src/Action/NotifyAction.php (handles the webhook). These two files define 90% of the runtime behavior.
  4. bin/console debug:router | grep payum — the capture / notify / after routes you paste into Stripe.

First use case — a one-off charge. Execute Capture with a Payum token; the action creates a Stripe Checkout Session, writes the session id into payment.details, and replies with an HTTP redirect to checkout.stripe.com. Card entry, 3-D Secure and SCA all happen on Stripe's hosted page.

use Payum\Core\Payum;
use Payum\Core\Reply\HttpRedirect;
use Payum\Core\Request\Capture;

public function pay(Payum $payum, string $token): Response
{
    $gateway = $payum->getGateway('stripe_checkout_session');
    $reply   = $gateway->execute(new Capture($token));

    if ($reply instanceof HttpRedirect) {
        return new RedirectResponse($reply->getUrl());
    }

    // Payment was already captured — show the order instead of re-charging.
    return $this->redirectToRoute('order_complete');
}

Implementation Patterns

Capture vs Authorize. Capture maps to Stripe's capture_method: automatic — money moves immediately. Use Authorize when you only want to charge on shipment: it creates a manual-capture PaymentIntent, and you later execute Capture on the authorized payment to settle it. Pick one per payment method; mixing them in the same gateway config confuses refund/void flows.

In Sylius you rarely touch Payum directly. Enable the Stripe Checkout Session factory on the payment method, paste the three keys into the gateway-config form, and let Sylius' checkout run capture → redirect → after-url. Fulfilment belongs in sylius.order.post_complete / sylius.payment.post_complete listeners, not in the gateway code. If you are on Symfony 8, verify the Sylius/Payum versions your Sylius release allows before bumping the bundle to v3.0.1.

The webhook is the source of truth, the redirect is UX. Users close tabs, lose connectivity, or hit the back button on Stripe's page. Grant access / ship goods on checkout.session.completed or payment_intent.succeeded (via NotifyAction), or on an explicit Status/Sync request that queries Stripe by the stored session id. Treat the success_url return as a nice-to-have.

Idempotency is your job. Stripe delivers at least once and retries on any non-2xx. Guard domain listeners by checking $payment->isCaptured() / details['status'] before fulfilling, and add a unique index on your order reference. The bundle's actions are usually idempotent; your listeners are usually not.

Metadata for reconciliation. Whatever CaptureAction puts into the Session (or PaymentIntent) metadata comes back in the webhook payload. Add order_id / customer_id there — via a small decorator around the Stripe client or the action — so the notify handler can resolve your domain object without guessing.

Queue the post-payment work. Don't do emails, ERP sync or PDF generation inside the notify controller; dispatch a Symfony Message from a listener. Keeps the webhook response under Stripe's timeout and gives you retries via Messenger.

Refunds/voids. These aren't the bundle's focus. Read the PaymentIntent id out of payment.details, then call \Stripe\Refund::create(...) in your own service (or a custom Payum Refund action). Wrap it so the Stripe call is retryable and logged.

Local testing.

stripe login
stripe listen --forward-to https://myapp.test/payment/notify
stripe trigger checkout.session.completed

Cards worth memorizing: 4242 4242 4242 4242 (success), 4000 0025 0000 3155 (3DS required), 4000 0000 0000 0002 (declined), 4000 0000 0000 9995 (insufficient funds). Always run one end-to-end with stripe listen running — that's the only way to see whether your notify route actually resolves the payment.

Gotchas and Tips

  • Signature verification failures. SignatureVerificationException almost always means webhook_secret is wrong — test vs live, or the endpoint-level secret vs the account default. It also happens when a proxy strips the Stripe-Signature header; check trusted_proxies and any nginx config rewriting headers.
  • Double fulfilment. Webhook and return-URL both confirm. Without an idempotency check you ship twice, email twice, and double-credit loyalty points. State checks plus a DB unique constraint.
  • Abandoned sessions. Checkout Sessions expire after 24 hours, but the Payum payment stays Pending forever. Add a scheduled command that voids/expires stale payments (Stripe's session-expire API) so your order pipeline doesn't fill with zombies.
  • URL generation behind a proxy. success_url / cancel_url are absolute. Without framework.trusted_proxies + trusted_headers, Symfony emits http://… and Stripe refuses it in live mode. Symptom: works locally, fails in staging.
  • Test/live key mixing. A pk_test_ publishable key with an sk_live_ secret produces "No such Checkout Session" / "No such customer" rather than a clean auth error. Validate that all three env vars share the same prefix.
  • Amounts and zero-decimal currencies. Payum's TotalAmount must be the integer minor-unit value, and CurrencyCode must be present. Missing currency or an amount in the wrong exponent (JPY, KRW) fails at the Stripe API, not in your code.
  • Re-executing Capture. An already-captured payment shouldn't create a second Session. Guard it in the controller/service — otherwise a user who refreshes the payment page generates an orphan session and can pay twice.
  • Authorization expiry. Manual-capture PaymentIntents expire (7 days for cards by default). Capture on shipment promptly, or you lose the auth and have to email the customer for a new one.
  • Template and CSP. The bundle renders a redirect view. Sylius themes and strict CSPs are the usual culprits if the user lands on a blank page. Override under templates/bundles/... and keep the redirect instant — no JS round-trip needed.
  • Debugging toolkit. debug:router | grep payum for the endpoints, debug:container | grep -i stripe for services and the factory, \Stripe\Stripe::setLogger($logger) at debug level in dev, and the Stripe Dashboard's webhook page for delivery attempts and response bodies. Between those four you can diagnose almost anything.
  • RequestNotSupported. Usually means the gateway factory isn't registered (wrong factory: name, bundle not in bundles.php, or config not loaded). Confirm against debug:container --tag=payum.gateway_factory before touching the actions.
  • Version/compat matrix. v3.0.1 is a patch release whose only change is Symfony 8 support — there are no code or API changes, so upgrading from v3.0.0 is low risk. The real trap is the reverse: on Symfony 8 the bundle resolves only at >=3.0.1, and a composer update that moves Symfony without moving the bundle will produce confusing container/Payum errors rather than a clean conflict. Pin the bundle and bump Symfony and the bundle in the same commit.
  • Package identity and bus factor. The upstream FLUX-SE/PayumStripeBundle is still actively released (v3.0.1), so it is not abandoned — but a Sylius-maintained fork also exists, and the two can diverge. composer show to see which you have, pin a major, and read the changelog when you cross majors. Dependency count is tiny and the docs are thin — budget an hour reading src/Action/ before you extend anything non-standard.
  • Extension points. Add behaviour (extra Session options, metadata, payment_method_types) by decorating CaptureAction or the Stripe client service, not by forking the bundle. A decorator that injects metadata['order_id'] into SessionCreateOptions is typically all you need — and that metadata is what makes the webhook handler trivial six months later.
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