FastAPI
Read the raw body with await request.body() and pass request.headers, which
matches names in any case.
pip install fastapi uvicorn ricochet-webhooksimport jsonimport os
from fastapi import FastAPI, HTTPException, Requestfrom ricochet_webhooks import WebhookVerificationError, verify
app = FastAPI()SECRET = os.environ["RICOCHET_WEBHOOK_SECRET"]handled: set[str] = set() # use your database in production
@app.post("/webhooks")async def webhooks(request: Request): body = await request.body() try: verified = verify(body, request.headers, SECRET) except WebhookVerificationError as error: raise HTTPException(status_code=401, detail=error.reason)
if verified.id in handled: return {"ok": True} event = json.loads(body) print("received", event.get("type")) handled.add(verified.id) return {"ok": True}Don’t declare the body as a Pydantic model on this route: FastAPI would parse it before
the check. Parse it yourself after verify.
verify also takes a list of secrets, for a rotation:
verify(body, request.headers, [new_secret, old_secret]).
Try it locally
Section titled “Try it locally”uvicorn main:app --port 3000ricochet listen --forward localhost:3000/webhooksDeliveries through the CLI carry no webhook-signature (a CLI destination has no
signing secret), so turn the check off in local development, for example with an
environment variable. Never skip it just because the header is missing: that would
accept unsigned requests in production too.