Receiving Stripe Webhooks: Endpoint Setup & Signature Verification
Stripe webhooks keep your backend in sync with payment events, subscription lifecycle updates, and billing changes. This guide covers endpoint implementation, dashboard registration, signature verification, testing flows, and the operational playbook Stripe recommends for high-volume webhook consumers.
Step 1: Build a Webhook Endpoint
Your endpoint can be written in any language as long as it accepts HTTPS POST requests and reads the raw request body for signature verification. Respond quickly with a 2xx status code before handing off heavy work to background jobs so Stripe does not time out delivery.
- Expose a stable path such as
/webhooks/stripe. - Parse JSON payloads cautiously—store the raw body for signature checks.
- Keep latency below 10 seconds; Stripe will retry if the endpoint is slow or returns non-2xx responses.
# Ruby + Sinatra example
post '/webhook' do
payload = request.body.read
event = nil
begin
event = Stripe::Event.construct_from(JSON.parse(payload, symbolize_names: true))
rescue JSON::ParserError
status 400
return
end
# TODO: handle the event by type
status 200
endStripe maintains official server libraries for Ruby, Node.js, Python, Go, Java, and PHP; reuse them to reduce low-level parsing work.
Step 2: Register the Endpoint in Stripe
Add the endpoint via the Stripe Dashboard under Developers → Webhooks or create it programmatically through the API. Stripe requires a publicly accessible HTTPS URL in live mode.
- Click Create an event destination and paste your HTTPS endpoint URL.
- Select the event types you actually process—examples include
payment_intent.succeeded,charge.failed, andcustomer.created. - Copy the signing secret that Stripe generates. Store it as
STRIPE_WEBHOOK_SECRETin your environment.
Step 3: Verify Stripe Signatures
Stripe signs each webhook with a shared secret and sends the signature in the Stripe-Signature header. Always validate the signature using the raw request body before trusting the payload, and reject events older than five minutes to prevent replay attacks.
# Ruby signature verification
endpoint_secret = ENV.fetch('STRIPE_WEBHOOK_SECRET')
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
begin
event = Stripe::Webhook.construct_event(payload, sig_header, endpoint_secret)
rescue Stripe::SignatureVerificationError => e
puts "Signature verification failed: #{e.message}"
halt 400
end// Node.js + Express example
import express from "express";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_API_KEY!);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["stripe-signature"];
try {
const event = stripe.webhooks.constructEvent(
req.body,
signature as string,
endpointSecret,
);
handleStripeEvent(event);
res.status(200).send("OK");
} catch (error) {
console.error("Invalid Stripe signature", error);
res.status(400).send("Webhook Error");
}
},
);Signature checklist
- Always use the raw body buffer; JSON middleware breaks signature validation.
- Compare timestamps inside
Stripe-Signatureand reject events older than 300 seconds. - Rotate secrets when you regenerate them in the Dashboard and deploy code updates simultaneously.
Step 4: Test with Stripe CLI & Hooklistener
Stripe offers multiple testing tools. Use them alongside Hooklistener to forward events into local environments, inspect payloads, and replay failures safely.
Stripe CLI
- Run
stripe listen --forward-to localhost:4242/webhooks/stripeto pipe events to your local server. - Trigger sample events with
stripe trigger payment_intent.succeededorstripe trigger invoice.paid. - Check delivery history in the Dashboard to debug failures.
Hooklistener Tunnels
- Capture live Stripe events with Hooklistener and forward them to your staging or local environment.
- Replay stored payloads to reproduce edge cases without waiting for Stripe to resend.
- Share event history across engineering and support teams for faster incident response.
Step 5: Handle Events Idempotently
Stripe delivers events at least once and occasionally out of order. Always gate processing on event.type and record event IDs you already handled so retries do not double-charge customers or create duplicate records.
import type Stripe from "stripe";
function handleStripeEvent(event: Stripe.Event) {
const alreadyProcessed = hasEventBeenHandled(event.id);
if (alreadyProcessed) {
return;
}
switch (event.type) {
case "payment_intent.succeeded":
fulfillSubscription(event.data.object);
break;
case "customer.created":
syncCustomer(event.data.object);
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
markEventHandled(event.id);
}- Persist processed event IDs in a durable data store (database or cache with TTL).
- Fetch canonical objects from Stripe’s API when accuracy matters (for example, re-fetch the payment intent by ID).
- Queue heavy work (email, invoicing, ERP sync) so your webhook response remains fast.
Step 6: Operational Best Practices
Security & Compliance
- Use HTTPS in live environments; Stripe enforces TLS for production endpoints.
- Exclude the webhook route from CSRF middleware in Rails, Django, or Laravel.
- Limit subscribed events to the minimal set your integration needs.
Reliability
- Stripe retries failed events for up to three days; monitor delivery logs for repeated failures.
- Expose health checks and alerting around webhook latency and error rates.
- Roll webhook secrets periodically and update your infrastructure in lockstep.
Key Steps & Tooling Overview
| Step | Action | Notes |
|---|---|---|
| Create Endpoint | Implement /webhooks/stripe | Accept POST, store raw body, respond quickly |
| Register Endpoint | Dashboard or API | Use HTTPS, subscribe only to required events |
| Verify Events | Validate Stripe-Signature | Use endpoint secret, enforce timestamp tolerance |
| Test Locally | Stripe CLI + Hooklistener | Forward events, replay payloads, inspect logs |
| Handle Events | Switch on event.type | Persist IDs to guarantee idempotency |
Build Faster with Hooklistener
Hooklistener captures Stripe webhooks in real time, forwards them to your development environments, and preserves an audit trail for replay. Spend less time wiring tunnels and more time shipping payment features.