How to Set Up Stripe Webhooks
Stripe webhooks notify your application when events happen in your Stripe account — successful payments, failed charges, subscription changes, disputes, and more. Here is how to set them up from scratch.
Step 1: Get a Webhook Endpoint
You need a publicly reachable URL that Stripe can send events to. For development and monitoring, the fastest approach is dread:
# Install dread
curl -sSL dread.sh/install | sh
# Create a channel
dread init
# You will get a URL like:
# https://dread.sh/wh/ch_abc123?source=stripe
For production, your webhook endpoint is a route in your application:
// Node.js / Express
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
return res.status(400).send('Webhook signature verification failed');
}
switch (event.type) {
case 'payment_intent.succeeded':
console.log('Payment succeeded:', event.data.object.id);
break;
case 'payment_intent.payment_failed':
console.log('Payment failed:', event.data.object.id);
break;
case 'customer.subscription.deleted':
console.log('Subscription cancelled:', event.data.object.id);
break;
}
res.status(200).json({received: true});
});
Step 2: Configure in the Stripe Dashboard
- Go to Developers → Webhooks in your Stripe Dashboard
- Click Add endpoint
- Paste your webhook URL
- Select the events you want to receive (start with the essentials):
Essential Events to Monitor
payment_intent.succeeded— a payment was completedpayment_intent.payment_failed— a payment attempt failedcharge.refunded— a refund was issuedcustomer.subscription.created— new subscription startedcustomer.subscription.updated— subscription plan changedcustomer.subscription.deleted— subscription cancelledinvoice.payment_failed— recurring payment failedcharge.dispute.created— a chargeback was filed
Step 3: Verify Webhook Signatures
Always verify the Stripe-Signature header to confirm the event came from Stripe. This prevents attackers from sending fake events to your endpoint.
# Python
import stripe
@app.route('/webhooks/stripe', methods=['POST'])
def stripe_webhook():
payload = request.data
sig = request.headers.get('Stripe-Signature')
try:
event = stripe.Webhook.construct_event(
payload, sig, endpoint_secret
)
except stripe.error.SignatureVerificationError:
return 'Invalid signature', 400
# Process the event
handle_event(event)
return '', 200
Never skip signature verification, even in development. It is the only way to confirm that Stripe — not an attacker — sent the event.
Step 4: Handle Retries and Idempotency
Stripe retries failed webhook deliveries for up to 3 days with exponential backoff. Your handler must be idempotent — processing the same event twice should not cause problems.
// Store processed event IDs to prevent duplicates
const processedEvents = new Set();
function handleEvent(event) {
if (processedEvents.has(event.id)) {
return; // Already processed
}
processedEvents.add(event.id);
// ... process the event
}
In production, use a database instead of an in-memory set.
Step 5: Monitor in Real Time
Once your webhooks are configured, use dread to monitor them live. You will get desktop notifications for every event and can inspect payloads in the terminal UI:
# Watch for events with desktop notifications
dread watch
# Or open the full terminal UI
dread
# Forward to Slack for team visibility
dread config --slack https://hooks.slack.com/services/xxx
Common Stripe Webhook Errors
- 400 — Signature verification failed: Check that you are using the correct webhook signing secret (not your API key)
- Timeout: Stripe expects a response within 20 seconds. Move heavy processing to a background job
- Duplicate events: Stripe may send the same event more than once. Always check idempotency
Monitor Stripe webhooks from your terminal
Get real-time payment notifications with one command.
curl -sSL dread.sh/install | sh