Documentation Menu

Stripe Webhooks Architecture & Validation

Stripe relies entirely on asynchronous webhooks to inform your application about payment states (e.g., payment_intent.succeeded). Because payments are critical path, Stripe enforces strict delivery and validation requirements. If you do not understand their retry mechanics or HMAC signature scheme, your system will drop payments or succumb to replay attacks.

The Most Common Stripe Bug: "No Signatures Found"

When verifying Stripe signatures in Node.js, the stripe.webhooks.constructEvent() function requires the exact raw bytes of the HTTP request body. If you use express.json() before verifying the webhook, Express mutates the raw stream into a parsed object. The signature verification will instantly fail.

The Fix: Use express.raw()

app.post('/webhook/stripe', express.raw({type: 'application/json'}), (req, res) => { ... });

Idempotency & Retry Mechanics

Stripe expects your endpoint to return an HTTP 200 OK within a few seconds. If your database write is slow, or your server crashes, Stripe will exponentially retry the webhook for up to 3 days. This creates a severe risk of duplicate processing.

  • Always decouple processing: Return a 200 immediately, and push the event to a background queue (e.g., Redis/BullMQ).
  • Track Event IDs: Store the Stripe event.id in your database. Before processing a payment, verify you haven't seen this event ID before.

Handling Timing Attacks

Stripe includes a timestamp in their Stripe-Signature header. By default, the SDK rejects events older than 5 minutes. This prevents attackers from capturing a valid webhook (e.g., "Grant premium access") and replaying it hours later.

Gotcha: If your server clock is heavily skewed, or if you are using a slow proxy, legitimate Stripe events will be rejected with a Tolerance error.

Was this page helpful?

Your feedback helps us improve the docs.