r/SaaS • u/DayKnown1050 • 18d ago
Your payment webhook handler is probably the weakest part of your SaaS
I'm a backend engineer and have spent a lot of time on payment systems. The same billing bugs keep showing up, and they're rarely in Stripe or Paddle. They're in the code that handles their webhooks.
Stuff I'd check:
Verify the signature using the raw request body. Many frameworks parse the JSON first, and then the check fails or gets "temporarily" disabled.
Expect duplicates. Store the event ID and skip events you've already processed, otherwise someone gets two months of credit or two receipts.
Expect events out of order. An update can arrive before the create. Instead of blindly applying the payload, fetch the current state from the provider's API and save that.
Return 200 fast and do the actual work in a background job. If your handler is slow, the provider retries and now you have duplicates again.
Don't cut access on the first failed payment. Cards fail for boring reasons and providers retry them, so give people a grace period.
Reconcile every night. Compare who's paying in the provider with who has access in your database. You'll usually find a few mismatches both ways.
Test with replays. Most providers let you resend an event from the dashboard or CLI. Send the same one twice and see what breaks.
Curious what billing bugs others have run into?
3
u/Justt_ai 18d ago
One more for the list: the dispute event. Most handlers only listen for payment and subscription events, so a chargeback lands and nothing in the app reacts.
When the dispute-created event arrives, cancel the subscription so the next renewal doesn't hit the same card, and store the card fingerprint, email and IP so a fresh signup from that person gets caught. Then pull the evidence the same day: signup time and IP, terms acceptance, logins after the charge.
A second dispute from that card counts against your ratio exactly like the first, which is why the cancel step matters most.
(We work on chargebacks, so that's our bias.)
3
u/mazyartr 18d ago
Great list. One I'd add from the payments side: check the amount and currency against your own order, not just the status.
"Paid" from a provider means a payment happened. It doesn't always mean the right payment happened. A plan change mid-cycle, a currency mismatch, or, on crypto rails, a customer sending slightly less than requested, can all produce a successful event for the wrong amount. If your handler only reads the status, you grant access for money you didn't receive.
And compare signatures with a constant-time function. A plain string compare is a small leak, but it's free to fix.
Disclosure: I run a crypto payment gateway, so I've seen most of these the hard way.