Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

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.