FastAPI + Stripe webhook signature verification: working versions (tested 2026-10-15)
Verify Stripe webhook signatures in FastAPI from the raw request body and dispatch on event type. Tested offline with locally signed payloads.
passed3 passed · 0 failed2026-10-15 11:35 UTC- Status
- passed
- Last passed
- 2026-10-15 11:35 UTC
- Tests
- 3 / 3
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38728226240
The code
Exactly the files that passed. Your agent can pull them with get_recipe("fastapi-stripe-webhook") (install), or see them on GitHub.
fastapi==0.143.1 stripe==16.0.0 httpx==0.28.1 pytest==9.1.1
import os
import stripe
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
WEBHOOK_SECRET = os.environ.get("STRIPE_WEBHOOK_SECRET", "whsec_test")
handled: list[str] = []
@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request, stripe_signature: str | None = Header(default=None)):
if not stripe_signature:
raise HTTPException(400, "missing signature")
payload = await request.body() # raw bytes, never re-serialized JSON
try:
event = stripe.Webhook.construct_event(payload, stripe_signature, WEBHOOK_SECRET)
except (ValueError, stripe.SignatureVerificationError):
raise HTTPException(400, "bad signature")
if event["type"] == "checkout.session.completed":
handled.append(event["data"]["object"]["id"])
return {"received": True, "type": event["type"]}
import hashlib, hmac, json, time
from fastapi.testclient import TestClient
from app.main import app, handled, WEBHOOK_SECRET
client = TestClient(app)
PAYLOAD = json.dumps({"id": "evt_1", "object": "event", "type": "checkout.session.completed", "data": {"object": {"id": "cs_1"}}})
def sign(payload: str, secret: str) -> str:
ts = int(time.time())
sig = hmac.new(secret.encode(), f"{ts}.{payload}".encode(), hashlib.sha256).hexdigest()
return f"t={ts},v1={sig}"
def test_valid_signature():
r = client.post("/webhooks/stripe", content=PAYLOAD, headers={"stripe-signature": sign(PAYLOAD, WEBHOOK_SECRET), "content-type": "application/json"})
assert r.status_code == 200 and r.json()["type"] == "checkout.session.completed"
assert "cs_1" in handled
def test_bad_signature():
r = client.post("/webhooks/stripe", content=PAYLOAD, headers={"stripe-signature": sign(PAYLOAD, "whsec_wrong")})
assert r.status_code == 400
def test_missing_signature():
assert client.post("/webhooks/stripe", content=PAYLOAD).status_code == 400
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
FastAPI + Pydantic v2 validators, discriminated unions and serializers
Pydantic v2 patterns in FastAPI that agents often get wrong: field_validator, model_validator, discriminated unions, computed_field and response_model filtering.
FastAPI + PyJWT bearer auth
OAuth2 password flow in FastAPI issuing PyJWT tokens, with a current-user dependency guarding routes.
FastAPI + SQLAlchemy 2 CRUD with dependency-injected sessions
FastAPI routes backed by SQLAlchemy 2.x typed models, a per-request session dependency, and TestClient tests on SQLite.
FastAPI background tasks + dependency overrides
FastAPI BackgroundTasks for post-response work, with dependency_overrides to swap services in tests.