Stripe Integration Mistakes: Retries, Fulfillment, and Subscriptions
Seven common Stripe integration mistakes that cause duplicate charges, missed fulfillment, and revenue loss — and how to fix each one before they hit production.
Payment integrations need to handle timeouts, retries, abandoned browser sessions, and subscription changes. Test those paths alongside successful checkout to catch duplicate actions and missed fulfillment.
Common Stripe Integration Mistakes That Cause Revenue Loss
1. Fulfilling orders based on the success URL redirect
The success_url parameter tells Stripe where to send the customer after a successful payment. Many teams wire their fulfillment logic — provisioning access, updating the database, sending the confirmation email — to this redirect.
The problem: the redirect is not guaranteed. If the customer's network drops after payment completes, the browser never loads the success URL. The payment went through, but your system never saw it.
Fix: Use verified webhooks and a concurrency-safe fulfillment function. Check the Checkout Session's payment_status and handle checkout.session.async_payment_succeeded for delayed methods. A landing page can also invoke the same server-side function, but must not be the only trigger. Stripe fulfillment guidance.
2. Not verifying webhook signatures
Shipping a webhook endpoint that accepts any POST request means an attacker can send fabricated events — invoice.payment_succeeded with a custom customer ID — and trigger fulfillment without paying.
Fix: Call stripe.webhooks.constructEvent() on every incoming webhook and reject anything that fails signature verification with a 400 response.
3. Processing webhooks synchronously inside the request lifecycle
Slow webhook handlers can time out and trigger retries. Persist the event durably, return a success response promptly, and process the work asynchronously.
Fix: Verify the signature, write the raw event to a queue or database table, return 200, and process asynchronously in a separate worker.
4. No idempotency guard on webhook handlers
Even a fast, async handler will be called more than once. Stripe's retry policy delivers events multiple times if your endpoint returns anything other than a 2xx. Without idempotency guards, a transient 500 on your side can cause a customer to be provisioned twice, sent two invoices, or have their account state corrupted.
Fix: Track receipt, processing, and completion separately. Use unique event IDs to coordinate concurrent workers, but mark work complete only after its effects are committed. Retain a retry path for failed attempts. Stripe webhook operations.
5. Missing subscription lifecycle events
Teams that ship a working checkout flow often stop there. But subscriptions have ongoing lifecycle events — renewals, failures, cancellations, plan changes — that require your system to stay in sync with Stripe.
Common gaps:
- No handler for
invoice.payment_failed— customer's card declines on renewal, but they retain full access - No handler for
customer.subscription.deleted— customer cancels through the Portal, but your database still shows them as active - No handler for
customer.subscription.updated— customer upgrades through the Portal, but your system does not reflect the new plan
Fix: Map every subscription lifecycle event to a database update. At minimum: updated, deleted, invoice.payment_failed, and invoice.payment_succeeded.
6. Generating idempotency keys at retry time instead of at request creation
If your retry logic generates a new uuid() on each attempt and uses it as the idempotency key, every retry is treated as a new request by Stripe. You get no duplicate protection. A customer whose payment times out mid-flight can be charged multiple times.
Fix: Generate the idempotency key once, from a deterministic source tied to the business operation (e.g., order_${orderId}_payment), persist it before making the Stripe API call, and reuse it on every retry for that operation.
7. Mixing Test and Live Configuration
Separate testing credentials, webhook destinations, and application configuration from live payment handling.
Fix: For new integrations, use isolated Stripe sandboxes for development and CI. Verify that each environment uses its own keys and webhook signing secret. Stripe sandboxes.
A Pattern Worth Adopting
The diagram is a simplified event flow. A durable implementation also needs explicit processing status and retry handling:
- Receive the event and verify its signature.
- Persist it durably with a unique event ID before acknowledging it.
- Let a worker claim pending work without treating receipt as completion.
- Apply idempotent business effects and record completion.
- Retry failed work and alert on unresolved failures.
Verify that queue persistence, worker retries, and fulfillment updates remain consistent when any step fails.