Mastering Stripe Webhooks: Avoid Double Charges with Idempotency
May 7, 2026
▶ Watch the 60-second version on YouTube
Understanding Stripe's At-Least-Once Delivery Guarantee
Stripe guarantees at-least-once delivery for webhooks, meaning that any event you subscribe to may be sent multiple times. This behavior is crucial for ensuring that you don’t miss critical events like payment confirmations, but it also introduces potential pitfalls if not handled correctly. If your webhook processing logic doesn’t account for idempotency, you risk double charging customers, issuing duplicate refunds, or creating other inconsistencies in your financial records.
The Importance of Idempotency
Idempotency is a concept that ensures that an operation can be applied multiple times without changing the result beyond the initial application. In the context of Stripe webhooks, this means that if your webhook handler processes the same event multiple times due to retries, it should not cause unintended side effects, such as creating duplicate charges or credits.
A common misconception is that simply checking if an event has already been processed is sufficient. While this is part of it, you must also design your application to handle the same event idempotently to ensure data integrity.
Implementing Idempotency in Your Webhook Handler
To implement idempotency in your webhook handler, you need to follow these steps:
- Store the unique event ID that Stripe sends with each webhook in your database.
- Before processing the event, check if this ID already exists.
- If it exists, skip processing; if not, perform the action and store the ID.
Example Webhook Handler in Python
Here’s an example of how you can implement this in a Flask application:
from flask import Flask, request, jsonify
import stripe
from your_database_module import store_event_id, has_event_id
app = Flask(__name__)
stripe.api_key = 'your_stripe_secret_key'
@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.get_data(as_text=True)
sig_header = request.headers.get('Stripe-Signature')
# Verify the webhook signature
event = None
try:
event = stripe.Webhook.construct_event(payload, sig_header, 'your_endpoint_secret')
except ValueError as e:
# Invalid payload
return jsonify({'error': str(e)}), 400
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return jsonify({'error': str(e)}), 400
event_id = event['id']
# Check if this event has already been processed
if has_event_id(event_id):
return jsonify({'status': 'already processed'}), 200
# Process the event
if event['type'] == 'payment_intent.succeeded':
payment_intent = event['data']['object'] # Contains a StripePaymentIntent
# Implement your charge logic here
# Store the event ID to prevent duplicate processing
store_event_id(event_id)
return jsonify({'status': 'success'}), 200
if __name__ == '__main__':
app.run(port=5000)
Non-Obvious Gotcha: Event Types and Their Consequences
One common oversight developers make is underestimating the variety of event types sent by Stripe. For example, if you're only processing `payment_intent.succeeded`, you may miss out on important events like `payment_intent.payment_failed` or `charge.refunded`. These events can be critical for maintaining accurate accounting and user experience. Always have a comprehensive list of events you need to handle and ensure your webhook can accommodate new ones as your application grows.
Database Design for Storing Event IDs
When implementing idempotency, the choice of your storage mechanism is crucial. A simple table structure might suffice:
CREATE TABLE processed_events (
event_id VARCHAR(255) PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Ensure you have a unique constraint on the `event_id` field to avoid duplicate entries, which can lead to confusion and data integrity issues. Regularly clean up old records to keep your database performant and manageable.
Testing Your Webhook Implementation
Testing is vital. Use Stripe's CLI or dashboard to send test webhooks and ensure your application behaves as expected. Validate that it correctly handles duplicates and that your idempotency logic works under various scenarios.
- Test with valid payloads and signature.
- Test with replayed events to confirm idempotency.
- Test edge cases like network failures during processing.
Monitoring and Logging
Finally, implement monitoring and logging for your webhook endpoints. This will help you identify issues early on, especially if unexpected events are occurring. Using tools like Sentry or Rollbar can help you capture errors and provide insights into how your webhook is functioning in production.
By following these best practices and understanding the nuances of at-least-once delivery, you can create a robust webhook handling system that minimizes the risks of double charges and other issues. Always stay alert to updates from Stripe as they enhance their platform, ensuring your implementation remains aligned with best practices.
💳 Best card for API and cloud spend — earn rewards on every Stripe, AWS, and OpenAI charge.