The two directions this breaks
A webhook is the message Stripe sends your app after something happens: a payment succeeded, a subscription renewed, a card failed, a customer cancelled. Your app does not watch Stripe. It trusts that message to update accounts. When the message path breaks, it breaks in one of two directions, and both are bad in different currencies.
The first direction costs you customers. Someone pays, Stripe sends the message, and it goes to an address that no longer answers, or the app ignores it because it was built for a different event list. The customer has a receipt and a locked account, and their first experience of your support is asking why they paid for a wall.
The second direction costs you the product. The endpoint accepts any request that looks like a payment message, without checking Stripe's signature. Anyone who finds that address can tell your app a payment happened when it did not, and be upgraded for free. This is not a theoretical attack. It is one ordinary request, and it is the single most common breaking finding on Ground Crew audits of apps that charge money.
The usual causes
The webhook URL still points at a preview or staging address from when the app was built, because Stripe's settings live outside the builder and nothing updated them when the real domain went on. The signing secret in the app belongs to a different endpoint than the one receiving events, often because the endpoint was recreated and the app was not. Test mode and live mode got crossed, so live payments produce events the app treats as test noise. Or the handler works exactly once: a retried event, which Stripe sends whenever your app is slow to answer, grants access twice, sends three emails, or errors halfway and leaves the account in between states.
Check yours without touching live payments
Work in Stripe's test mode, which exists for exactly this. In the Stripe dashboard, open your webhook endpoint and read three things: the URL it sends to, whether it matches your live domain exactly, and the list of events it is subscribed to compared with the ones your app depends on. Then use the dashboard's test event or the Stripe CLI to send a test payment succeeded event and watch what your app does with a test account. Finally, check the signature story: send the same event without a valid signature, or better, ask the code directly whether the endpoint verifies signatures before it trusts an event. If an unsigned request can change an account, that is the finding, and it outranks everything else on your list.
One honest note on fixing: this is guidance and a verification test, not a copy and paste prompt, because a wrong change to a billing endpoint can break live payments or look fixed while staying open. Verify signatures first, check the endpoint URL and event list second, and handle retries so the same event processed twice lands in the same state, third.
Verify it: in test mode, a completed payment upgrades the test account within a few seconds. An unsigned or badly signed request changes nothing and gets an error in response. Sending the same genuine event twice leaves the account in exactly the state one delivery would.
FAQ
My customers say they paid but their account never upgraded. Where do I look first?
The webhook endpoint in Stripe. Check that its URL is your live domain, that it is subscribed to the events your app needs, and that Stripe's own delivery log for that endpoint shows successful responses rather than errors or timeouts.
What does signature verification actually stop?
It stops anyone other than Stripe from convincing your app that a payment event happened. Without it, your paid tier is protected by the hope that nobody finds the address.
Stripe keeps retrying an event and my users get duplicate emails. Is that the same problem?
It is the cousin problem, called idempotency. Your handler should recognise an event it has already processed and finish quietly instead of running its side effects again.
Ground Crew note
Payments and webhooks are check 04 on the free production audit, and the first thing we look at on any app that charges money. The full list is on the post launch checklist, or you can request the free audit and we will test it in Stripe test mode with you.