// Practical guide
Stripe webhook security
Why the webhook is critical
The webhook is where your SaaS learns a payment happened. If it accepts any request, an attacker forges an event and unlocks paid access without paying. If it processes the same event twice, you deliver twice or credit wrongly.
The good news: the two core defenses — signature and idempotency — are simple and remove most of the risk.
Signature verification
Every event must be validated with the endpoint secret before any effect. Use the raw request body to check the signature; if your framework parses the JSON first, verification fails silently.
- Verify the signature with the endpoint signing secret.
- Use the raw body, not the already-parsed JSON.
- Reject events without a valid signature with a 400 and no side effects.
Idempotency and ordering
Providers resend events and do not guarantee order. Store the processed event id and ignore repeats. Treat state as convergent: what matters is the subscription’s final state, not the exact arrival sequence.
- Record the event id and ignore reprocessing.
- Make the handler idempotent: running twice = same result.
- Do not assume order; reconcile from current provider state when in doubt.
Unlock access at the right moment
Unlock the plan only after real payment confirmation — especially for Pix, boleto, and async methods, where "success" on screen does not mean paid. The success page is UX; the webhook is the source of truth.
Frequently asked questions
Can I unlock access on the success page?
Not as a source of truth. For async methods (Pix, boleto), the payment may not have settled. Unlock via the confirmed webhook; use the success page only to guide the user.
Why does signature verification fail sometimes?
Almost always because the body was parsed (or modified) before the check. The signature needs the raw body exactly as received.
Do I really need idempotency?
Yes. Providers resend events by design. Without idempotency, you risk double delivery, wrong credit, or access unlocked more than once.