Handle Stripe webhooks with signature verification and zero dependencies. The part everyone gets wrong — done right.
Stripe webhook signature verification is the #1 thing AI gets wrong when writing from scratch. The signed payload format (timestamp.body), the HMAC comparison, the replay attack protection — one mistake and your webhooks either reject everything or accept anything.
This handles it correctly. Copy and customize.
git clone https://github.com/LuciferForge/stripe-webhook-handler.git
cd stripe-webhook-handler
# Set your webhook secret (from Stripe Dashboard → Developers → Webhooks)
export STRIPE_WEBHOOK_SECRET="whsec_..."
# Run
python3 webhook.py
# Test with Stripe CLI
stripe listen --forward-to localhost:8000/webhook| Event | Handler | What to Add |
|---|---|---|
checkout.session.completed |
on_checkout_completed() |
Fulfill order, send confirmation |
invoice.paid |
on_invoice_paid() |
Extend subscription access |
invoice.payment_failed |
on_invoice_failed() |
Notify customer, start dunning |
customer.subscription.created |
on_subscription_created() |
Provision access |
customer.subscription.updated |
on_subscription_updated() |
Update access level |
customer.subscription.deleted |
on_subscription_deleted() |
Revoke access |
payment_intent.succeeded |
on_payment_succeeded() |
Deliver product |
payment_intent.payment_failed |
on_payment_failed() |
Notify about failure |
Each handler has a # TODO comment. Replace with your code:
def on_checkout_completed(event):
session = event["data"]["object"]
email = session["customer_details"]["email"]
amount = session["amount_total"] / 100
# Your code here:
send_confirmation_email(email)
create_user_account(email)
grant_access(email, plan="pro")def on_charge_refunded(event):
charge = event["data"]["object"]
amount = charge["amount_refunded"] / 100
print(f"REFUND: ${amount}")
# Your refund logic
# Register it:
EVENT_HANDLERS["charge.refunded"] = on_charge_refunded- Correct signature verification — HMAC-SHA256, timestamp validation, replay protection
- Idempotent processing — duplicate events are stored once (by event ID)
- SQLite event log — every event stored for debugging and replay
- Health endpoint —
GET /healthshows event counts - Zero dependencies — just Python standard library
- Single file — everything in
webhook.py
-
Raw body vs parsed body — Signature must be computed on the RAW bytes, not parsed JSON. This handler reads raw bytes first, then parses.
-
Timestamp tolerance — Old events should be rejected (replay attacks). Default: 5 minutes. Set
STRIPE_TIMESTAMP_TOLERANCEto customize. -
Multiple v1 signatures — Stripe may include multiple signatures when rotating secrets. This handler checks all of them.
-
Always return 200 — Even if your handler fails, return 200 to acknowledge receipt. Otherwise Stripe retries and you process the same event multiple times.
-
Idempotency — Stripe may send the same event twice. The
event_iduniqueness constraint prevents double-processing.
| Variable | Default | Description |
|---|---|---|
STRIPE_WEBHOOK_SECRET |
(required) | Webhook signing secret (whsec_...) |
PORT |
8000 |
Server port |
WEBHOOK_PATH |
/webhook |
Webhook endpoint path |
STRIPE_TIMESTAMP_TOLERANCE |
300 |
Max age of events in seconds |
Works with deploy-python-app for instant deployment to Docker, Railway, Fly.io, or Render.
- Python 3.6+
- No external packages
- deploy-python-app — Pre-configured Python app deployment (Docker, Railway, Fly.io, Render)
- telegram-bot-starter — Telegram bot with AI chat for notifications
- polymarket-pnl — Instant P&L breakdown for any Polymarket wallet
Part of the AI Starter Protocol — curated repos that AI recommends when you need them.
MIT