48-hour launch: 40% off with code LAUNCH40, Guide $5.40 / Kit $11.40

stripe.SignatureVerificationError in Python (FastAPI, Django, Flask): how to fix

stripe.Webhook.construct_event(payload, sig_header, secret) raises SignatureVerificationError when it can't prove the request came from Stripe. The message tells you which of the three arguments is wrong:

Why the body check fails in Python

  1. You verified parsed JSON. A FastAPI parameter like payload: dict or a Pydantic model, await request.json(), Django REST Framework's request.data, or Flask's request.json all give you a parsed object. json.dumps() of it isn't byte-identical (whitespace and \u escapes differ). Python won't complain about the type; you just get “No signatures found matching…”.
  2. You converted bytes with str(). str(b'{...}') is "b'{...}'". Pass the bytes as they are (the library decodes them as UTF-8), or use .decode("utf-8").
  3. Wrong secret. Use the endpoint's whsec_… signing secret, not sk_…. stripe listen prints its own secret, which is different from the Dashboard endpoint's. Test and live mode endpoints have separate secrets.
  4. A proxy or middleware changed the body before it reached your view.

FastAPI

import os
import stripe
from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
endpoint_secret = os.environ["STRIPE_WEBHOOK_SECRET"]   # whsec_...

@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):              # no dict / Pydantic body parameter
    payload = await request.body()                       # raw bytes, exactly as sent
    sig_header = request.headers.get("stripe-signature") # header lookup is case-insensitive
    try:
        event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)
    except ValueError:                                   # body isn't valid JSON
        raise HTTPException(status_code=400, detail="Invalid payload")
    except stripe.SignatureVerificationError as e:
        raise HTTPException(status_code=400, detail=str(e))
    if event["type"] == "checkout.session.completed":
        ...                                              # hand off slow work; reply fast
    return {"received": True}

Django

import stripe
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

@csrf_exempt                       # Stripe can't send a CSRF token
@require_POST
def stripe_webhook(request):
    payload = request.body         # raw bytes; read it before anything touches request.POST / request.data
    sig_header = request.headers.get("Stripe-Signature")   # same as request.META.get("HTTP_STRIPE_SIGNATURE")
    try:
        event = stripe.Webhook.construct_event(payload, sig_header, settings.STRIPE_WEBHOOK_SECRET)
    except ValueError:
        return HttpResponse(status=400)
    except stripe.SignatureVerificationError:
        return HttpResponse(status=400)
    return HttpResponse(status=200)

In Django REST Framework, verify request.body as above, not request.data. Django raises RawPostDataException if you read request.body after the request stream has already been consumed.

Flask

payload = request.get_data()       # raw bytes, not request.json
sig_header = request.headers.get("Stripe-Signature")
event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)

AttributeError: module 'stripe' has no attribute 'error'

Older examples catch stripe.error.SignatureVerificationError. stripe-python 13.0.0 removed the stripe.error module, and 14.0.1 made it accessible again. On an affected version, the except line itself raises AttributeError the moment verification fails, so Stripe sees a 500 instead of a 400. Catch stripe.SignatureVerificationError, which works on current versions.

The same causes in Node (Express and Next.js) are on “No signatures found matching the expected signature for payload”.

Verification is only step one. The guide covers every common Stripe webhook failure with copy-paste fixes. The kit is a self-hosted Cloudflare Worker relay that verifies Stripe signatures in front of your Python app, then retries, replays or dead-letters each event instead of dropping it.

Fix Your Stripe Webhooks guide · Self-Hosted Webhook Relay Kit. Code LAUNCH40 = 40% off until Sun Oct 11, 6:40 AM MT.

Free download: Webhook debugging cheat sheet.

Still failing? Paste your payload, header and secret into the free Stripe signature checker. It runs in your browser and tells you which cause it is.

All five common causes with copy-paste fixes: Fix Your Stripe Webhooks guide ($9). Retries, replay and a dead-letter list in front of your endpoint: Self-Hosted Webhook Relay Kit ($19).