A test payment went through clean. Card accepted, Stripe's dashboard showed the subscription sitting there active, green tick, nothing wrong anywhere in sight. I refreshed the app expecting to see the account flip over to the paid plan.
It didn't. Still showing as free.
That's an unsettling kind of bug, because normally when something breaks, something complains about it. An error shows up in a log, a request comes back with a status code that isn't 200, anything. This had none of that. Stripe was happy. My own server was happy. The webhook that's supposed to notify the app when a payment lands had received the event, run through it, and reported back that everything had processed successfully. Only nothing had actually happened.
So I went looking in the one place that couldn't lie to me: the raw data the webhook had actually received, instead of what I assumed it looked like. And there it was, hiding in plain sight. A piece of information my code expected to find in one particular spot on that payload just wasn't there anymore. Not missing exactly, more like it had moved house without leaving a forwarding address. The payment provider's API had been quietly updated at some point, and the fields I'd built this whole flow around had been relocated somewhere else in the response.
My code wasn't crashing on that, which is really the whole problem. It had been written defensively enough that when it looked for something and found nothing, it just shrugged and carried on, no harm done as far as it was concerned. That's exactly what you want your code to do when a value is truly optional. It's a disaster when the value wasn't optional at all, it had just wandered off somewhere else without telling anyone.
Fixing it was the easy part once I'd actually tracked it down: read from where the data lived now instead of where it used to. What stuck with me was less the fix itself and more the reminder that "no error" isn't the same thing as "it worked." Some bugs shout. This one just smiled, said everything was fine, and let a real payment sit there doing nothing for a good while before anyone noticed.