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:
No signatures found matching the expected signature for payload: the body isn't the exact bytes Stripe sent, or the secret is wrong. This is the common one; see below.No Stripe-Signature header value was provided: sig_header is None or empty. See No stripe-signature header value was provided.Unable to extract timestamp and signatures from header: the second argument isn't a t=…,v1=… string, often because the arguments are swapped. See that page.No webhook secret value was provided. It should start with `whsec_`: your env var didn't load.Timestamp outside the tolerance zone: the signature is valid but older than 300 seconds (the default). See timestamp outside tolerance.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…”.str(). str(b'{...}') is "b'{...}'". Pass the bytes as they are (the library decodes them as UTF-8), or use .decode("utf-8").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.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}
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.
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).