Skip to content

FastAPI

Read the raw body with await request.body() and pass request.headers, which matches names in any case.

Terminal window
pip install fastapi uvicorn ricochet-webhooks
main.py
import json
import os
from fastapi import FastAPI, HTTPException, Request
from 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]).

Terminal window
uvicorn main:app --port 3000
ricochet listen --forward localhost:3000/webhooks

Deliveries 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.