Webhooks: The Source of Truth
The webhook endpoint
@shop_bp.post("/webhooks/stripe")
@csrf.exempt # Stripe cannot send your CSRF token
def stripe_webhook():
payload = request.get_data() # the RAW body, exactly as received
sig = request.headers.get("Stripe-Signature", "")
try:
event = stripe.Webhook.construct_event(payload, sig, os.environ["STRIPE_WEBHOOK_SECRET"])
except (ValueError, stripe.error.SignatureVerificationError):
return "", 400
if ProcessedEvent.query.get(event["id"]): # idempotency
return "", 200
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
if session["payment_status"] == "paid":
fulfil_order(session)
elif event["type"] == "charge.refunded":
revoke_access_for_charge(event["data"]["object"])
db.session.add(ProcessedEvent(id=event["id"]))
db.session.commit()
return "", 200
Rules
- Verify the signature with the raw request body. Parsing the JSON first and re-serialising breaks verification.
- Idempotency: Stripe retries webhooks and can deliver the same event more than once. Record processed event IDs and make fulfilment safe to repeat.
- Respond quickly (2xx within a few seconds); do slow work in a background job.
- Exempt only this route from CSRF; the signature is what authenticates it.
- Handle events arriving in any order. If fulfilment depends on data that is not ready, make the logic tolerant rather than assuming order.
Fulfilment
def fulfil_order(session):
user_id = int(session["metadata"]["user_id"])
project_id = int(session["metadata"]["project_id"])
if not DownloadAccess.query.filter_by(user_id=user_id, project_id=project_id).first():
db.session.add(DownloadAccess(user_id=user_id, project_id=project_id,
stripe_session_id=session["id"]))
queue_receipt_email(user_id, project_id)
Testing locally with the Stripe CLI
stripe login
stripe listen --forward-to localhost:5000/webhooks/stripe # prints a whsec_ secret for local use
stripe trigger checkout.session.completed
Choosing events
Subscribe only to events you handle. Common ones: checkout.session.completed, checkout.session.async_payment_succeeded / _failed (for delayed payment methods), invoice.paid, invoice.payment_failed, customer.subscription.updated / .deleted, charge.refunded, charge.dispute.created.
Exercise
Implement the webhook with signature verification, idempotent processing and fulfilment that grants download access, then test it end to end with stripe listen and a test payment, including a duplicate delivery.
Check yourself
- Why must you verify signatures against the raw body?
- How do you handle duplicate webhook deliveries?
- Why exempt only the webhook route from CSRF?
Tu progreso se guarda solo en este navegador.
Enviar para revisión (10 puntos)