Stripe Integration Pitfalls: What We Have Learned from 20+ Implementations

Stripe Integration Pitfalls: What We Have Learned from 20+ Implementations 1024 504 admin

Stripe is, by a significant margin, the best-documented payment API available for eCommerce developers. The documentation is accurate, the test environment is realistic, and the dashboard tooling is genuinely useful.

Stripe integrations still fail. Not because the documentation is wrong, but because the edge cases that cause production failures are not the ones you encounter while reading the guide.

After building and reviewing Stripe integrations across more than twenty eCommerce implementations — on Magento, WooCommerce, Shopify, and custom platforms — the failure patterns are consistent enough to document. This post is a summary of what we have seen and how to avoid it.

Webhook Delivery: Why Idempotency Is Not Optional

Stripe’s webhook delivery guarantees at-least-once delivery, not exactly-once. The same event will be sent multiple times if your webhook endpoint does not respond with a 200 within Stripe’s timeout window, or if Stripe retries due to a delivery failure on their side. This is documented. It is also the source of the most common production bugs we see in Stripe integrations.

If your webhook handler for payment_intent.succeeded triggers order fulfillment, and the same event is delivered twice, you may attempt to fulfill the same order twice. For physical goods, this creates a duplicate shipment. For digital goods, a duplicate delivery. For subscription activations, a duplicate account creation.

Every webhook handler must be idempotent. Before processing an event, check whether you have already processed it — by storing processed event IDs in your database and returning 200 immediately if the event ID is already recorded. This is not an optimization. It is a correctness requirement.

Event Ordering: Why You Cannot Assume Events Arrive in Sequence

Stripe does not guarantee that events arrive in chronological order. A charge.succeeded event may arrive after the charge.refunded event for the same charge. A customer.subscription.updated event may arrive before the customer.subscription.created event that preceded it.

Designing webhook handlers that assume events arrive in order — processing a refund only if the original charge is already recorded, for example — will fail intermittently and in ways that are difficult to reproduce. Instead, design handlers to be order-independent: record each event’s data into your system, then derive the current state from the totality of events received rather than from an assumed sequence.

Alternatively, when an event references an object whose current state matters (a charge, a subscription, a payment intent), fetch the current state from the Stripe API at processing time rather than relying on the event payload — which reflects the object state at event emission time, which may have already changed by the time the event arrives.

PaymentIntent vs Charge and When Each Applies

The PaymentIntents API is Stripe’s current recommendation for most payment flows, and it handles the full lifecycle of a payment including authentication (3DS), authorization, and capture. The older Charges API does not handle authentication flows and is no longer recommended for new integrations.

The nuance that causes confusion: PaymentIntents are not one-to-one with charges. A single PaymentIntent can involve multiple charge attempts — for example, if the first attempt fails authentication and the customer retries with a different card. Your order management logic should be keyed to the PaymentIntent ID, not the Charge ID, to correctly handle multi-attempt payment flows.

For multi-step payment flows — authorize now, capture on shipment — the PaymentIntent capture_method of manual holds the authorization until you explicitly capture it. Authorization holds expire after seven days by default. If your fulfillment SLA exceeds seven days, you need to handle authorization re-capture or the payment will fail at capture time.

Partial Capture and the Reconciliation Problems It Creates

Stripe allows partial capture — capturing less than the authorized amount when the final order total differs from the authorized total. This is useful when items are removed from an order between authorization and fulfillment. It is also a reliable source of reconciliation discrepancies.

The issue: the authorized amount and the captured amount are different. Your accounting integration, your settlement reporting, and your order management system all need to handle this correctly. Systems that assume captured amount equals authorized amount will produce incorrect financial reporting for any order with a partial capture.

If your platform supports order modifications after authorization, explicitly design for partial capture in your accounting and reconciliation flows. Log both the authorized amount and the captured amount. Reconcile against captured amounts, not authorized amounts.

Refund Architecture: Do Not Depend on Automatic Refunds

Stripe processes refunds promptly and reliably under normal conditions. “Under normal conditions” excludes high dispute rate periods, bank processing delays, and the edge case where a refund fails because the original charge was disputed and the funds are held pending dispute resolution.

Integrations that assume refunds always succeed immediately — updating order state to “refunded” before confirming the refund succeeded via the charge.refunded webhook — will occasionally show customers a refunded order that has not actually been refunded. This creates customer service problems and potential chargeback exposure.

Initiate the refund, set the order state to “refund pending,” and confirm the refund completion via the webhook event before updating the order state to “refunded.” This is more complex state management, but it accurately reflects what is actually happening.

Subscription Edge Cases: Trials, Proration, and Dunning

Stripe Billing is powerful, but subscription state management has edge cases that are not apparent from the basic documentation. Trial-to-paid conversion timing, proration behavior when customers change plans mid-cycle, and dunning configuration interact in ways that produce unexpected behavior if not explicitly tested.

Trial end behavior: when a trial ends and the first invoice is generated, the customer.subscription.updated event fires before the invoice is paid. If your activation logic depends on invoice payment, not trial status change, you need to handle the transition between these two events correctly. A customer whose trial converts but whose first payment fails should have a different account state than a customer who was never on a trial.

Dunning: Stripe’s Smart Retries use machine learning to retry failed charges at times with higher success probability. This is generally beneficial, but if your platform notifies customers about failed charges immediately on failure, you may be sending failed payment notifications for charges that Stripe would have successfully retried a few hours later. Coordinate your customer notification timing with Stripe’s retry schedule.

Rate Limiting Under Promotional Traffic Spikes

Stripe’s API rate limits are generous under normal operating conditions and become relevant primarily during promotional events — flash sales, Black Friday, product launches — when API call volume spikes sharply.

Common patterns that generate disproportionate API call volume: polling payment status on the client side rather than using webhooks, loading Stripe customer and payment method data on every page load rather than caching it, and creating a new PaymentIntent for every checkout session initiation rather than reusing existing intents for incomplete payments.

Review your API call patterns before a high-traffic event. Implement exponential backoff on 429 responses. Cache Stripe customer and payment method data on your side rather than fetching it from Stripe on every request. These changes reduce your API call volume by 60-70% under typical eCommerce traffic patterns.

Testing Stripe in Staging: What the Test Environment Does Not Replicate

Stripe’s test environment replicates the API surface accurately. It does not replicate production-level latency, production-level rate limiting behavior, or the specific failure modes that occur under high concurrent payment volume.

Test your error handling with Stripe’s test card numbers for specific failure modes: 4000000000000002 for card declined, 4000000000009995 for insufficient funds, 4000002760003184 for authentication required. Test your webhook handler idempotency by delivering the same event multiple times to your test endpoint. Test your retry logic by simulating API timeouts.

What you cannot test in staging: the latency distribution of real production payment authorizations, the behavior of specific card issuer banks under 3DS authentication, and the edge cases in Stripe’s fraud detection that only manifest with real cardholder data. Production monitoring — specifically, tracking authorization rates by card type and geography — will surface these issues faster than any staging environment.