Billing + idempotent webhooks
Billing is Stripe Checkout + the customer portal, driven by signature-verified webhooks. The webhook handler is idempotent: the event is processed first, recorded only on success, and any failure rolls back and returns 500 so Stripe safely retries. A duplicate or re-delivered event is not processed twice.
# backend/app/api/v1/webhooks.py (excerpt)event_id = event["id"]event_type = event["type"]
if await is_event_processed(session, event_id): return {"status": "already_processed"}
handler = handlers.get(event_type)if handler is None: await mark_event_processed(session, event_id, event_type, json.dumps(event)) await session.commit() return {"status": "ignored"}
try: await handler(session, event["data"]["object"]) await mark_event_processed(session, event_id, event_type, json.dumps(event)) await session.commit()except Exception: await session.rollback() raise HTTPException( status_code=500, detail="Error processing webhook (Stripe will retry)", )
return {"status": "processed"}The endpoint is documented in the API Reference (POST /api/v1/webhooks/stripe).
Replay safety is covered by backend/tests/test_edge_cases.py, which posts a re-delivered
event_id and asserts the handler returns already_processed with a 200, not a second charge.
Checkout and portal sessions are the other two billing endpoints (/api/v1/billing/checkout
and /api/v1/billing/portal), both behind authentication.