The four ways a Stripe integration goes wrong in production
Most Stripe failures are not Stripe bugs. They are payment systems built without understanding what happens when things go half-done: webhooks fire twice, subscriptions charge wrong prices, disputes land in a void, and tax bills show up in January.
Stripe integrations are deceptively simple to get working. The test mode passes. Money flows. But production catches you in ways that do not surface until you have thousands of customers and your calendar is booked for three months.
We have shipped Stripe integrations for thirty startups. The failures are always the same four. This post defends the thesis that you can avoid all of them with one principle: assume every webhook fires twice, subscriptions change mid-month, platforms have fraud, and your tax jurisdiction is ambiguous.
Failure mode 1: Webhooks without idempotency
Stripe webhooks are reliable. They retry for 3 days. They are also not atomic. A webhook can arrive twice at 145 milliseconds apart. Your `payment.success` handler runs twice before the database saves the first result.
The symptom: a customer deposits $100. Your webhook processes it. The network hiccup. Stripe retries. The webhook processes it again. Now your database has two $100 deposits. You do not notice until the customer reports a duplicate charge.
The fix is idempotency. Every Stripe event has an `id` field. That `id` never changes. Store it. Check it. Only process each event once.
// Node.js + PostgreSQL example
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
// Check if we've processed this event before
const existing = await db.query(
'SELECT id FROM stripe_events WHERE event_id = $1',
[event.id]
);
if (existing.rows.length > 0) {
return res.json({received: true}); // Already processed
}
// Process the event
if (event.type === 'charge.succeeded') {
await db.query(
'INSERT INTO stripe_events (event_id, event_type) VALUES ($1, $2)',
[event.id, event.type]
);
// Now handle the charge
await processChargeSucceeded(event.data.object);
}
res.json({received: true});
});
Stripe gives you 30 days to acknowledge a webhook. In that window, any webhook can be replayed. Use the Stripe API to fetch the full event object rather than trusting the webhook payload. The payload can be tampered with. The API fetch is the source of truth.
Most teams do not do this. The cost is low. The benefit is infinite. One duplicate charge detection saved a marketplace $8,500 in January 2024 when a payment processor hiccup sent webhooks in bursts.
Failure mode 2: Subscriptions without proration math
Subscriptions seem simple until mid-month changes. A customer upgrades from $30 per month to $50 per month on day 15. How much should they pay. $10 for the remaining 15 days. Or nothing, because it is their problem. Or they get credited $5 for overpaying if they already paid.
Stripe does not guess. You tell Stripe what to do with the `proration_behavior` flag. Three options exist:
- create_prorations: Calculate the difference and bill or credit accordingly.
- none: Ignore proration. Charge the full new price at the next billing date.
- always_invoice: Create a prorated invoice immediately. The customer sees the new charge today.
The cost difference is significant. A customer who upgrades partway through a month might see a $5 credit or a $20 charge, depending on your choice. Neither is obvious from the code.
You need one more thing: understanding what a prorated invoice item looks like. When you query Stripe for an invoice, prorations show up as separate line items with negative amounts. Your billing dashboard needs to display these. Most teams hide them. Customers see their bill and do not understand why their credit card was charged half of what they expected.
Failure mode 3: Connect platforms without dispute routing
If you run a marketplace, you use Stripe Connect. Sellers' cards are connected to your account. Stripe holds the money. You decide how to split it. Revenue flows from buyer to your account to seller. You set the fee percentage.
Then a seller gets charged back. The customer claims they never received the goods. The chargeback comes to your Stripe account first. Now you own the relationship with both the seller and the customer. And you own the fraud risk.
The mistake: not implementing a dispute webhook handler. You receive the `charge.dispute.created` event. You do nothing. Sixty days later, you lose $5k. Stripe covers nothing. You never told the seller to defend themselves.
The fix is a three-step process.
- Listen to `charge.dispute.created` events. Extract the seller's ID from the charge metadata.
- Notify the seller immediately. Give them 7 days to respond. If they have evidence (tracking number, signed receipt), they can submit it to Stripe via the API.
- Log the dispute in your database. Mark the transaction as contested. Do not pay the seller until the dispute is resolved. Stripe takes 45-75 days to resolve each dispute.
Stripe Radar can also help. It flags high-risk transactions before they complete. Set it to `block` mode for your highest-risk verticals. It is slower than pure fraud rules, but it catches issues your rules miss.
Failure mode 4: Tax compliance left to the last week
You launch in the US. No one thinks about tax. Two years later you expand to Europe. VAT MOSS applies. You now need to file 27 tax returns. It is March. Taxes are due April 20. You have 30 days.
The mistake is not deciding early what you will do. Stripe Tax exists. It costs $0.005 per calculation. For a $1M SaaS company, that is under $200 per month.
| Approach | Cost (tax logic) | Time to build | When to choose it |
|---|---|---|---|
| Stripe Tax | $0.005 per calc | 2-3 days | Most teams, all stages |
| Custom + Avalara | $100 per mo base | 6-8 weeks | High complexity, 50+ nexus |
| Build custom logic | $0 direct | 12-16 weeks | Only if you have very specific rules |
Stripe Tax handles US sales tax, EU VAT, UK VAT, and India GST. It integrates with Stripe Billing. You call one API. Stripe calculates, collects, and files for you. In 2026, you do not build this yourself.
The India GST case is interesting. India allows a 5 percent margin scheme for certain verticals. You need to file returns showing what you collected and what tax you owe. Stripe Tax does not do this. You need a separate integration with Razorpay or a local CA. It is doable. But it is a distinct project.
The pre-launch checklist we run
Before any Stripe integration goes live, we verify all of these in order.
- Webhook idempotency key stored before any business logic runs.
- Stripe Signature header validated. We verify the signature is not older than 5 minutes.
- All webhook types tested in production sandbox: charge, refund, dispute, invoice lifecycle, subscription events.
- Proration behavior explicitly chosen for all subscription plans. Documented in code comments.
- Dunning logic implemented: failed charges retry with exponential backoff. Stop after 5 failures, notify the user.
- Subscription cancellation: when a customer cancels, we check if they have a balance credit. If so, we refund it. If not, we do nothing.
- Prorated invoice items displayed correctly in the customer dashboard. No surprises.
- For Connect platforms: dispute webhook handler deployed. Seller notification sent within 1 hour of dispute creation.
- Tax compliance: if selling to multiple regions, Stripe Tax or custom logic is live and tested.
- PCI scope validation: verify we are not storing raw card data anywhere. No card numbers in logs. No PAN in error messages.
- Stripe API version pinned in code. We do not auto-upgrade to the latest API.
- Webhook replay tested: manually trigger a test webhook in Stripe Dashboard. Verify idempotency key prevents duplicate processing.
Where this lives at Empyreal
Stripe integrations are part of our core. We ship Stripe integrations for SaaS startups, fintech platforms, and marketplaces. Every team we work with gets our pre-launch checklist. If you are building a payment system and want to avoid the four failure modes, we can help. See our Stripe integration service page. We also have deep fintech expertise. Read about fintech architecture or our SaaS architecture services. And if you are curious about payment terms and concepts, check our architecture glossary.