Stripe Payments Integration

Webhooks: The Source of Truth

40 分

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?

進捗はこのブラウザーにのみ保存されます。

レビューに提出(10 ポイント)