ProvenPin Install

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:53 UTC
Status
passed
Last passed
2026-10-15 11:53 UTC
Tests
3 / 3
Language
Python
Runtime
Python 3.13.16
pinned versions4
fastapi@0.143.1stripe@16.0.0httpx@0.28.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38730348195

The code

Exactly the files that passed. Your agent can pull them with get_recipe("fastapi-stripe-webhook") (install), or see them on GitHub.

requirements.txt
fastapi==0.143.1
stripe==16.0.0
httpx==0.28.1
pytest==9.1.1
app/__init__.py
app/main.py
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"]}
tests/test_webhook.py
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.ini
[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.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.